Merge remote-tracking branch 'origin/main' into paint-context

Nathan Sobo created

Change summary

.github/workflows/release_actions.yml                        |   13 
.zed/settings.json                                           |    5 
Cargo.lock                                                   |  428 
Cargo.toml                                                   |   12 
Dockerfile                                                   |    2 
assets/icons/file_icons/ai.svg                               |   27 
assets/icons/file_icons/archive.svg                          |    4 
assets/icons/file_icons/audio.svg                            |    4 
assets/icons/file_icons/camera.svg                           |    2 
assets/icons/file_icons/conversations.svg                    |    2 
assets/icons/file_icons/database.svg                         |    2 
assets/icons/file_icons/file.svg                             |    4 
assets/icons/file_icons/file_types.json                      |  332 
assets/icons/file_icons/folder.svg                           |    3 
assets/icons/file_icons/folder_open.svg                      |    5 
assets/icons/file_icons/git.svg                              |    2 
assets/icons/file_icons/image.svg                            |    2 
assets/icons/file_icons/lock.svg                             |    2 
assets/icons/file_icons/magnifying_glass.svg                 |    3 
assets/icons/file_icons/notebook.svg                         |    6 
assets/icons/file_icons/package.svg                          |    2 
assets/icons/file_icons/plus.svg                             |    3 
assets/icons/file_icons/prettier.svg                         |   10 
assets/icons/file_icons/project.svg                          |    5 
assets/icons/file_icons/replace.svg                          |    9 
assets/icons/file_icons/replace_all.svg                      |    2 
assets/icons/file_icons/replace_next.svg                     |    5 
assets/icons/file_icons/rust.svg                             |    1 
assets/icons/file_icons/settings.svg                         |    0 
assets/icons/file_icons/toml.svg                             |    2 
assets/icons/file_icons/typescript.svg                       |    2 
assets/icons/file_icons/video.svg                            |    2 
assets/keymaps/default.json                                  |   16 
assets/settings/default.json                                 |    4 
crates/ai/src/assistant.rs                                   |    1 
crates/collab/src/tests.rs                                   |   43 
crates/collab/src/tests/integration_tests.rs                 |   98 
crates/collab/src/tests/randomized_integration_tests.rs      |    2 
crates/collab_ui/src/collab_titlebar_item.rs                 |    2 
crates/collab_ui/src/contact_list.rs                         |    6 
crates/collab_ui/src/incoming_call_notification.rs           |   12 
crates/collab_ui/src/project_shared_notification.rs          |   16 
crates/collab_ui/src/sharing_status_indicator.rs             |    8 
crates/command_palette/src/command_palette.rs                |   24 
crates/context_menu/src/context_menu.rs                      |   22 
crates/copilot/src/copilot.rs                                |    4 
crates/copilot/src/sign_in.rs                                |   55 
crates/diagnostics/src/diagnostics.rs                        |   10 
crates/drag_and_drop/src/drag_and_drop.rs                    |   40 
crates/editor/Cargo.toml                                     |    3 
crates/editor/src/display_map/inlay_map.rs                   |    4 
crates/editor/src/editor.rs                                  |  210 
crates/editor/src/editor_tests.rs                            |  665 +
crates/editor/src/element.rs                                 |  124 
crates/editor/src/inlay_hint_cache.rs                        |   31 
crates/editor/src/items.rs                                   |    9 
crates/editor/src/test/editor_lsp_test_context.rs            |    5 
crates/editor/src/test/editor_test_context.rs                |   20 
crates/file_finder/src/file_finder.rs                        |  250 
crates/go_to_line/src/go_to_line.rs                          |    2 
crates/gpui/Cargo.toml                                       |    1 
crates/gpui/src/app.rs                                       |  517 
crates/gpui/src/app/menu.rs                                  |    6 
crates/gpui/src/app/ref_counts.rs                            |   14 
crates/gpui/src/app/test_app_context.rs                      |  267 
crates/gpui/src/app/window.rs                                |  189 
crates/gpui/src/app/window_input_handler.rs                  |   27 
crates/gpui/src/elements/resizable.rs                        |    3 
crates/gpui/src/platform.rs                                  |    8 
crates/gpui/src/platform/mac.rs                              |    2 
crates/gpui/src/platform/mac/platform.rs                     |   14 
crates/gpui/src/platform/mac/window.rs                       |   27 
crates/gpui/src/platform/test.rs                             |   40 
crates/gpui/src/scene/mouse_event.rs                         |    1 
crates/language/src/language.rs                              |   78 
crates/language_tools/src/lsp_log_tests.rs                   |    4 
crates/live_kit_client/build.rs                              |   12 
crates/node_runtime/src/node_runtime.rs                      |   44 
crates/project/src/project.rs                                |   29 
crates/project/src/project_tests.rs                          |   55 
crates/project/src/search.rs                                 |  117 
crates/project/src/terminals.rs                              |    6 
crates/project/src/worktree.rs                               |    2 
crates/project_panel/src/file_associations.rs                |   11 
crates/project_panel/src/project_panel.rs                    |  148 
crates/project_symbols/src/project_symbols.rs                |    5 
crates/recent_projects/src/highlighted_workspace_location.rs |    3 
crates/recent_projects/src/recent_projects.rs                |    3 
crates/search/Cargo.toml                                     |    1 
crates/search/src/buffer_search.rs                           |  257 
crates/search/src/project_search.rs                          |  553 +
crates/search/src/search.rs                                  |  191 
crates/semantic_index/Cargo.toml                             |   22 
crates/semantic_index/README.md                              |    0 
crates/semantic_index/src/db.rs                              |  204 
crates/semantic_index/src/embedding.rs                       |   34 
crates/semantic_index/src/parsing.rs                         |  321 +
crates/semantic_index/src/semantic_index.rs                  |  817 ++
crates/semantic_index/src/semantic_index_settings.rs         |   10 
crates/semantic_index/src/semantic_index_tests.rs            | 1679 ++++++
crates/sum_tree/src/cursor.rs                                |    3 
crates/sum_tree/src/sum_tree.rs                              |    8 
crates/terminal/Cargo.toml                                   |    2 
crates/terminal/src/mappings/colors.rs                       |    6 
crates/terminal/src/terminal.rs                              |    8 
crates/terminal_view/src/terminal_element.rs                 |    4 
crates/terminal_view/src/terminal_panel.rs                   |    8 
crates/terminal_view/src/terminal_view.rs                    |   12 
crates/theme/src/theme.rs                                    |    1 
crates/theme/src/ui.rs                                       |    1 
crates/util/src/paths.rs                                     |  118 
crates/vector_store/src/modal.rs                             |  172 
crates/vector_store/src/parsing.rs                           |  115 
crates/vector_store/src/vector_store.rs                      |  770 --
crates/vector_store/src/vector_store_tests.rs                |  161 
crates/vim/Cargo.toml                                        |    3 
crates/vim/src/editor_events.rs                              |    8 
crates/vim/src/mode_indicator.rs                             |  107 
crates/vim/src/normal/search.rs                              |    4 
crates/vim/src/test.rs                                       |   62 
crates/vim/src/test/vim_test_context.rs                      |    8 
crates/vim/src/vim.rs                                        |   10 
crates/workspace/src/dock.rs                                 |   11 
crates/workspace/src/item.rs                                 |   13 
crates/workspace/src/pane.rs                                 |   38 
crates/workspace/src/pane/dragged_item_receiver.rs           |   12 
crates/workspace/src/pane_group.rs                           |  169 
crates/workspace/src/searchable.rs                           |    6 
crates/workspace/src/status_bar.rs                           |   56 
crates/workspace/src/workspace.rs                            |  301 
crates/zed/Cargo.toml                                        |    5 
crates/zed/resources/app-icon-preview.png                    |    0 
crates/zed/resources/app-icon-preview@2x.png                 |    0 
crates/zed/src/languages.rs                                  |    2 
crates/zed/src/languages/bash/config.toml                    |    3 
crates/zed/src/languages/c/embedding.scm                     |   43 
crates/zed/src/languages/cpp/embedding.scm                   |   61 
crates/zed/src/languages/elixir/embedding.scm                |   27 
crates/zed/src/languages/go/embedding.scm                    |   24 
crates/zed/src/languages/javascript/embedding.scm            |  117 
crates/zed/src/languages/json/embedding.scm                  |   14 
crates/zed/src/languages/lua/config.toml                     |    1 
crates/zed/src/languages/lua/embedding.scm                   |   10 
crates/zed/src/languages/nix/config.toml                     |   11 
crates/zed/src/languages/nix/highlights.scm                  |   89 
crates/zed/src/languages/php/config.toml                     |    1 
crates/zed/src/languages/php/embedding.scm                   |   36 
crates/zed/src/languages/php/outline.scm                     |    7 
crates/zed/src/languages/ruby/config.toml                    |    1 
crates/zed/src/languages/ruby/embedding.scm                  |   22 
crates/zed/src/languages/rust.rs                             |    4 
crates/zed/src/languages/rust/config.toml                    |    1 
crates/zed/src/languages/rust/embedding.scm                  |   54 
crates/zed/src/languages/toml/config.toml                    |    2 
crates/zed/src/languages/tsx/embedding.scm                   |  110 
crates/zed/src/languages/typescript/embedding.scm            |  132 
crates/zed/src/main.rs                                       |   82 
crates/zed/src/zed.rs                                        |  197 
docs/theme/generating-theme-types.md                         |   29 
rust-toolchain.toml                                          |    2 
styles/src/style_tree/editor.ts                              |    4 
styles/src/style_tree/status_bar.ts                          |   34 
styles/src/theme/create_theme.ts                             |   37 
163 files changed, 7,854 insertions(+), 3,816 deletions(-)

Detailed changes

.github/workflows/release_actions.yml 🔗

@@ -6,14 +6,23 @@ jobs:
   discord_release:
     runs-on: ubuntu-latest
     steps:
+    - name: Get appropriate URL
+      id: get-appropriate-url
+      run: |
+        if [ "${{ github.event.release.prerelease }}" == "true" ]; then
+          URL="https://zed.dev/releases/preview/latest"
+        else
+          URL="https://zed.dev/releases/stable/latest"
+        fi
+        echo "::set-output name=URL::$URL"
+
     - name: Discord Webhook Action
       uses: tsickert/discord-webhook@v5.3.0
-      if: ${{ ! github.event.release.prerelease }}
       with:
         webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
         content: |
           📣 Zed ${{ github.event.release.tag_name }} was just released!
 
-          Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
+          Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
 
           ${{ github.event.release.body }}

Cargo.lock 🔗

@@ -36,11 +36,11 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.19.0"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
 dependencies = [
- "gimli 0.27.2",
+ "gimli 0.27.3",
 ]
 
 [[package]]
@@ -61,7 +61,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom 0.2.9",
+ "getrandom 0.2.10",
  "once_cell",
  "version_check",
 ]
@@ -88,9 +88,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
 dependencies = [
  "memchr",
 ]
@@ -118,57 +118,59 @@ dependencies = [
  "settings",
  "smol",
  "theme",
- "tiktoken-rs 0.4.2",
+ "tiktoken-rs 0.4.5",
  "util",
  "workspace",
 ]
 
 [[package]]
 name = "alacritty_config"
-version = "0.1.1-dev"
-source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed4261e640d3954fbdd9be45#a51dbe25d67e84d6ed4261e640d3954fbdd9be45"
+version = "0.1.2-dev"
+source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
 dependencies = [
  "log",
  "serde",
- "serde_yaml",
+ "toml 0.7.6",
+ "winit",
 ]
 
 [[package]]
 name = "alacritty_config_derive"
-version = "0.2.1-dev"
-source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed4261e640d3954fbdd9be45#a51dbe25d67e84d6ed4261e640d3954fbdd9be45"
+version = "0.2.2-dev"
+source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.28",
 ]
 
 [[package]]
 name = "alacritty_terminal"
-version = "0.17.1-dev"
-source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed4261e640d3954fbdd9be45#a51dbe25d67e84d6ed4261e640d3954fbdd9be45"
+version = "0.20.0-dev"
+source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
 dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
  "base64 0.13.1",
- "bitflags",
- "dirs 4.0.0",
+ "bitflags 2.3.3",
+ "home",
  "libc",
  "log",
  "mio 0.6.23",
  "mio-anonymous-pipes",
  "mio-extras",
  "miow 0.3.7",
- "nix",
+ "nix 0.26.2",
  "parking_lot 0.12.1",
  "regex-automata 0.1.10",
  "serde",
  "serde_yaml",
  "signal-hook",
  "signal-hook-mio",
+ "toml 0.7.6",
  "unicode-width",
  "vte",
- "winapi 0.3.9",
+ "windows-sys",
 ]
 
 [[package]]
@@ -177,16 +179,22 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
 [[package]]
 name = "alsa"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
+checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47"
 dependencies = [
  "alsa-sys",
- "bitflags",
+ "bitflags 1.3.2",
  "libc",
- "nix",
+ "nix 0.24.3",
 ]
 
 [[package]]
@@ -205,6 +213,36 @@ version = "0.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
 
+[[package]]
+name = "android-activity"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0"
+dependencies = [
+ "android-properties",
+ "bitflags 1.3.2",
+ "cc",
+ "jni-sys",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum 0.6.1",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
 [[package]]
 name = "android_system_properties"
 version = "0.1.5"
@@ -225,7 +263,7 @@ dependencies = [
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
- "is-terminal 0.4.7",
+ "is-terminal 0.4.9",
  "utf8parse",
 ]
 
@@ -250,7 +288,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys",
 ]
 
 [[package]]
@@ -260,14 +298,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
 dependencies = [
  "anstyle",
- "windows-sys 0.48.0",
+ "windows-sys",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.71"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
 
 [[package]]
 name = "arrayref"
@@ -283,9 +321,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
 name = "ascii"
@@ -306,9 +344,9 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
 dependencies = [
  "concurrent-queue",
  "event-listener",
@@ -324,7 +362,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "once_cell",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
  "tokio",
 ]
 
@@ -338,7 +376,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "memchr",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
 ]
 
 [[package]]
@@ -350,7 +388,7 @@ dependencies = [
  "async-lock",
  "async-task",
  "concurrent-queue",
- "fastrand",
+ "fastrand 1.9.0",
  "futures-lite",
  "slab",
 ]
@@ -362,7 +400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
 dependencies = [
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "futures-lite",
 ]
@@ -389,14 +427,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
 dependencies = [
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-lite",
  "log",
  "parking",
  "polling",
- "rustix 0.37.19",
+ "rustix 0.37.23",
  "slab",
  "socket2",
  "waker-fn",
@@ -418,7 +456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
 dependencies = [
  "async-io",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "futures-lite",
 ]
@@ -440,14 +478,14 @@ checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
 dependencies = [
  "async-io",
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "cfg-if 1.0.0",
  "event-listener",
  "futures-lite",
- "rustix 0.37.19",
+ "rustix 0.37.23",
  "signal-hook",
- "windows-sys 0.48.0",
+ "windows-sys",
 ]
 
 [[package]]
@@ -469,7 +507,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -482,7 +520,7 @@ dependencies = [
  "async-global-executor",
  "async-io",
  "async-lock",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -492,7 +530,7 @@ dependencies = [
  "log",
  "memchr",
  "once_cell",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
  "pin-utils",
  "slab",
  "wasm-bindgen-futures",
@@ -506,7 +544,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
 ]
 
 [[package]]
@@ -517,7 +555,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -554,13 +592,13 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.68"
+version = "0.1.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
+checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -573,7 +611,7 @@ dependencies = [
  "futures-io",
  "futures-util",
  "log",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
  "tungstenite 0.16.0",
 ]
 
@@ -588,12 +626,9 @@ dependencies = [
 
 [[package]]
 name = "atomic"
-version = "0.5.1"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
-dependencies = [
- "autocfg 1.1.0",
-]
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
 
 [[package]]
 name = "atomic-waker"
@@ -649,15 +684,6 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "autocfg"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
-dependencies = [
- "autocfg 1.1.0",
-]
-
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -673,19 +699,19 @@ dependencies = [
  "async-trait",
  "axum-core",
  "base64 0.13.1",
- "bitflags",
+ "bitflags 1.3.2",
  "bytes 1.4.0",
  "futures-util",
  "headers",
  "http",
  "http-body",
  "hyper",
- "itoa 1.0.6",
+ "itoa 1.0.9",
  "matchit",
  "memchr",
  "mime",
  "percent-encoding",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -726,7 +752,7 @@ dependencies = [
  "futures-util",
  "http",
  "mime",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
  "serde",
  "serde_json",
  "tokio",
@@ -738,16 +764,16 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.67"
+version = "0.3.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
+checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
 dependencies = [
- "addr2line 0.19.0",
+ "addr2line 0.20.0",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.6.2",
- "object 0.30.3",
+ "miniz_oxide 0.7.1",
+ "object 0.31.1",
  "rustc-demangle",
 ]
 
@@ -797,7 +823,7 @@ version = "0.64.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -817,7 +843,7 @@ version = "0.65.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -830,7 +856,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.18",
+ "syn 2.0.28",
  "which",
 ]
 
@@ -855,6 +881,27 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bitflags"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
 [[package]]
 name = "block"
 version = "0.1.6"
@@ -879,6 +926,25 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-sys"
+version = "0.1.0-beta.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146"
+dependencies = [
+ "objc-sys",
+]
+
+[[package]]
+name = "block2"
+version = "0.2.0-alpha.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42"
+dependencies = [
+ "block-sys",
+ "objc2-encode",
+]
+
 [[package]]
 name = "blocking"
 version = "1.3.1"
@@ -889,7 +955,7 @@ dependencies = [
  "async-lock",
  "async-task",
  "atomic-waker",
- "fastrand",
+ "fastrand 1.9.0",
  "futures-lite",
  "log",
 ]
@@ -974,21 +1040,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
 dependencies = [
  "memchr",
- "regex-automata 0.3.3",
+ "regex-automata 0.3.4",
  "serde",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.12.2"
+version = "3.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
 
 [[package]]
 name = "bytecheck"
-version = "0.6.10"
+version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f"
+checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
 dependencies = [
  "bytecheck_derive",
  "ptr_meta",
@@ -997,9 +1063,9 @@ dependencies = [
 
 [[package]]
 name = "bytecheck_derive"
-version = "0.6.10"
+version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5"
+checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1060,6 +1126,20 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "calloop"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8"
+dependencies = [
+ "bitflags 1.3.2",
+ "log",
+ "nix 0.25.1",
+ "slotmap",
+ "thiserror",
+ "vec_map",
+]
+
 [[package]]
 name = "cap-fs-ext"
 version = "0.24.4"
@@ -1168,15 +1248,21 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
 [[package]]
 name = "chrono"
-version = "0.4.24"
+version = "0.4.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
 dependencies = [
+ "android-tzdata",
  "iana-time-zone",
  "js-sys",
- "num-integer",
  "num-traits",
  "serde",
  "time 0.1.45",
@@ -1207,7 +1293,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
 dependencies = [
  "glob",
  "libc",
- "libloading",
+ "libloading 0.7.4",
 ]
 
 [[package]]
@@ -1217,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
  "atty",
- "bitflags",
+ "bitflags 1.3.2",
  "clap_derive 3.2.25",
  "clap_lex 0.2.4",
  "indexmap 1.9.3",
@@ -1229,24 +1315,23 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.3.5"
+version = "4.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
+checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d"
 dependencies = [
  "clap_builder",
- "clap_derive 4.3.2",
+ "clap_derive 4.3.12",
  "once_cell",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.3.5"
+version = "4.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
+checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1"
 dependencies = [
  "anstream",
  "anstyle",
- "bitflags",
  "clap_lex 0.5.0",
  "strsim",
 ]
@@ -1266,14 +1351,14 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.3.2"
+version = "4.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
+checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
 dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -1340,11 +1425,11 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.21",
+ "time 0.3.24",
  "tiny_http",
  "url",
  "util",
- "uuid 1.3.2",
+ "uuid 1.4.1",
 ]
 
 [[package]]
@@ -1368,7 +1453,7 @@ name = "cocoa"
 version = "0.24.0"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "block",
  "cocoa-foundation",
  "core-foundation",
@@ -1383,7 +1468,7 @@ name = "cocoa-foundation"
 version = "0.1.1"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "block",
  "core-foundation",
  "core-graphics-types",
@@ -1392,16 +1477,6 @@ dependencies = [
  "objc",
 ]
 
-[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
-
 [[package]]
 name = "collab"
 version = "0.16.0"
@@ -1452,10 +1527,10 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.21",
+ "time 0.3.24",
  "tokio",
  "tokio-tungstenite",
- "toml",
+ "toml 0.5.11",
  "tonic",
  "tower",
  "tracing",
@@ -1554,7 +1629,7 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
 dependencies = [
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1574,6 +1649,21 @@ dependencies = [
  "theme",
 ]
 
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "copilot"
 version = "0.1.0"
@@ -1645,7 +1735,7 @@ name = "core-graphics"
 version = "0.22.3"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "core-foundation",
  "core-graphics-types",
  "foreign-types",
@@ -1657,7 +1747,7 @@ name = "core-graphics-types"
 version = "0.1.1"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "core-foundation",
  "foreign-types",
  "libc",
@@ -1690,7 +1780,7 @@ version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "core-foundation-sys 0.6.2",
  "coreaudio-sys",
 ]
@@ -1740,9 +1830,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.7"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
 dependencies = [
  "libc",
 ]
@@ -1867,16 +1957,6 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
-[[package]]
-name = "crossbeam-channel"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "maybe-uninit",
-]
-
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.8"
@@ -1884,7 +1964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1895,19 +1975,19 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.14"
+version = "0.9.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
- "memoffset 0.8.0",
+ "crossbeam-utils",
+ "memoffset 0.9.0",
  "scopeguard",
 ]
 
@@ -1918,25 +1998,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-dependencies = [
- "autocfg 1.1.0",
- "cfg-if 0.1.10",
- "lazy_static",
+ "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.15"
+version = "0.8.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1988,9 +2057,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.61+curl-8.0.1"
+version = "0.4.65+curl-8.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
+checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986"
 dependencies = [
  "cc",
  "libc",
@@ -2002,60 +2071,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "cxx"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
-dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
-dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 2.0.18",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.94"
+name = "cursor-icon"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
+checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.18",
+ "serde",
 ]
 
 [[package]]
 name = "dashmap"
-version = "5.4.0"
+version = "5.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
+checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
 dependencies = [
  "cfg-if 1.0.0",
- "hashbrown 0.12.3",
+ "hashbrown 0.14.0",
  "lock_api",
  "once_cell",
- "parking_lot_core 0.9.7",
+ "parking_lot_core 0.9.8",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -63,7 +63,7 @@ members = [
     "crates/theme",
     "crates/theme_selector",
     "crates/util",
-    "crates/vector_store",
+    "crates/semantic_index",
     "crates/vim",
     "crates/vcs_menu",
     "crates/workspace",
@@ -79,6 +79,7 @@ resolver = "2"
 anyhow = { version = "1.0.57" }
 async-trait = { version = "0.1" }
 ctor = { version = "0.1" }
+derive_more = { version = "0.99.17" }
 env_logger = { version = "0.9" }
 futures = { version = "0.3" }
 globset = { version = "0.4" }
@@ -109,10 +110,10 @@ pretty_assertions = "1.3.0"
 
 tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
 tree-sitter-c = "0.20.1"
-tree-sitter-cpp = "0.20.0"
+tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
 tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
-tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" }
-tree-sitter-elm = "5.6.4"
+tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
+tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"}
 tree-sitter-embedded-template = "0.20.0"
 tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
@@ -131,9 +132,10 @@ tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", r
 tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"}
 tree-sitter-lua = "0.0.14"
+tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 
 [patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1c65ca24bc9a734ab70115188f465e12eecf224e" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
 
 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

Dockerfile 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.70-bullseye as builder
+FROM rust:1.71-bullseye as builder
 WORKDIR app
 COPY . .
 

assets/icons/file_icons/ai.svg 🔗

@@ -0,0 +1,27 @@
+<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.375 8.74577V10.375C7.30597 10.375 6.69403 10.375 4.625 10.375V10.1226L9.375 5.87742V5.625H4.625V7.27717" stroke="black" stroke-width="1.25"/>
+<circle cx="0.5" cy="8" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="1.49976" cy="5.82825" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="1.49976" cy="10.1719" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="13.5" cy="8.01581" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="12.5" cy="5.84387" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="12.5" cy="10.1877" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="6.99213" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="4.50391" cy="2.48438" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="2.49976" cy="3.48438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="2.49976" cy="12.5" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="0.5" cy="12.016" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="0.5" cy="3.98438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="13.5" cy="12.016" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="13.5" cy="3.98438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="2.49976" cy="14.516" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="2.48413" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="11.5" cy="14.516" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="11.5" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="11.5" cy="3.48438" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="11.5" cy="12.516" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="9.49609" cy="2.48438" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="6.99213" cy="14.5" r="0.5" fill="black" fill-opacity="0.3"/>
+<circle cx="4.50391" cy="13.516" r="0.5" fill="black" fill-opacity="0.6"/>
+<circle cx="9.49609" cy="13.5" r="0.5" fill="black" fill-opacity="0.6"/>
+</svg>

assets/icons/file_icons/archive.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 7.63H8" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<rect x="2" y="2" width="10" height="3" rx="0.5" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+<path d="M6 7.63H8" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="2" y="2" width="10" height="3" rx="0.5" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
 <path d="M2.59375 5H11.4375L10.5581 11.5664C10.5248 11.8146 10.313 12 10.0625 12H3.93944C3.68812 12 3.47585 11.8134 3.44358 11.5642L2.59375 5Z" stroke="black" stroke-width="1.25"/>
 </svg>

assets/icons/file_icons/audio.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M7 11C5.46973 11 4.1268 11.1873 3.31522 11.3327C2.94367 11.3992 2.60079 11.0563 2.66733 10.6848C2.81266 9.8732 3 8.53027 3 7C3 5.8387 2.89211 4.78529 2.77656 3.99011C2.73589 3.71017 3.19546 3.51715 3.36119 3.7464C4.09612 4.76304 5.23301 6.23301 6.5 7.5C7.76699 8.76699 9.23696 9.90388 10.2536 10.6388C10.4828 10.8045 10.2898 11.2641 10.0099 11.2234C9.21472 11.1079 8.1613 11 7 11Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.3594 3.35938C12.3594 3.35938 12.0146 2.9209 11.5312 2.4375C11.0479 1.9541 10.6406 1.64062 10.6406 1.64062" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M11.3516 7.36803C11.3516 7.36803 10.7962 5.88996 9.48438 4.57812C8.17254 3.26629 6.64062 2.64155 6.64062 2.64155" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M12.365 3.8478L10.3381 1.82088" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M11.3516 7.36803L6.64062 2.64155" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
 <rect x="2.72266" y="8.73828" width="3.58525" height="2.72899" rx="0.5" transform="rotate(45 2.72266 8.73828)" fill="black"/>
 </svg>

assets/icons/file_icons/camera.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V4.88C2 4.60386 2.22386 4.38 2.5 4.38H4.4342C4.61518 4.38 4.78204 4.2822 4.87046 4.12428L5.35681 3.25572C5.44524 3.0978 5.61209 3 5.79308 3H8.20692C8.38791 3 8.55476 3.0978 8.64319 3.25572L9.12954 4.12428C9.21796 4.2822 9.38482 4.38 9.5658 4.38H11.5C11.7761 4.38 12 4.60386 12 4.88V10.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10754 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10754 9 7.005 9Z" fill="black" fill-opacity="0.33" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10754 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10754 9 7.005 9Z" fill="black" fill-opacity="0.3" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/conversations.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.46115 8.43419C7.30678 8.43419 8.92229 7.43411 8.92229 5.21171C8.92229 2.98933 7.30678 1.98926 5.46115 1.98926C3.61553 1.98926 2 2.98933 2 5.21171C2 6.028 2.21794 6.67935 2.58519 7.17685C2.7184 7.35732 2.69033 7.77795 2.58387 7.97539C2.32908 8.44793 2.81048 8.9657 3.33372 8.84571C3.72539 8.75597 4.13621 8.63447 4.49574 8.4715C4.62736 8.41181 4.7727 8.38777 4.91631 8.40402C5.09471 8.42416 5.27678 8.43419 5.46115 8.43419Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="0.990499" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_icons/database.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<ellipse cx="7" cy="4" rx="5" ry="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+<ellipse cx="7" cy="4" rx="5" ry="2" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
 <path d="M12 4V10C12 11.1046 9.76142 12 7 12C4.23858 12 2 11.1046 2 10V4" stroke="black" stroke-width="1.25"/>
 <path d="M12 7C12 8.10457 9.76142 9 7 9C4.23858 9 2 8.10457 2 7" stroke="black" stroke-width="1.25"/>
 </svg>

assets/icons/file_icons/file.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 4H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 4H10" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M2 7H12" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M2 10H8" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 10H8" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/file_types.json 🔗

@@ -1,159 +1,179 @@
 {
-  "suffixes": {
-    "aac": "audio",
-    "bash": "terminal",
-    "bmp": "image",
-    "c": "code",
-    "conf": "settings",
-    "cpp": "code",
-    "cc": "code",
-    "css": "code",
-    "doc": "document",
-    "docx": "document",
-    "eslintrc": "eslint",
-    "eslintrc.js": "eslint",
-    "eslintrc.json": "eslint",
-    "flac": "audio",
-    "fish": "terminal",
-    "gitattributes": "vcs",
-    "gitignore": "vcs",
-    "gitmodules": "vcs",
-    "gif": "image",
-    "go": "code",
-    "h": "code",
-    "handlebars": "code",
-    "hbs": "template",
-    "htm": "template",
-    "html": "template",
-    "svelte": "template",
-    "hpp": "code",
-    "ico": "image",
-    "ini": "settings",
-    "java": "code",
-    "jpeg": "image",
-    "jpg": "image",
-    "js": "code",
-    "json": "storage",
-    "lock": "lock",
-    "log": "log",
-    "md": "document",
-    "mdx": "document",
-    "mp3": "audio",
-    "mp4": "video",
-    "ods": "document",
-    "odp": "document",
-    "odt": "document",
-    "ogg": "video",
-    "pdf": "document",
-    "php": "code",
-    "png": "image",
-    "ppt": "document",
-    "pptx": "document",
-    "prettierrc": "prettier",
-    "prettierignore": "prettier",
-    "ps1": "terminal",
-    "psd": "image",
-    "py": "code",
-    "rb": "code",
-    "rkt": "code",
-    "rs": "rust",
-    "rtf": "document",
-    "scm": "code",
-    "sh": "terminal",
-    "bashrc": "terminal",
-    "bash_profile": "terminal",
-    "bash_aliases": "terminal",
-    "bash_logout": "terminal",
-    "profile": "terminal",
-    "zshrc": "terminal",
-    "zshenv": "terminal",
-    "zsh_profile": "terminal",
-    "zsh_aliases": "terminal",
-    "zsh_histfile": "terminal",
-    "zlogin": "terminal",
-    "sql": "code",
-    "svg": "image",
-    "swift": "code",
-    "tiff": "image",
-    "toml": "toml",
-    "ts": "typescript",
-    "tsx": "code",
-    "txt": "document",
-    "wav": "audio",
-    "webm": "video",
-    "xls": "document",
-    "xlsx": "document",
-    "xml": "template",
-    "yaml": "settings",
-    "yml": "settings",
-    "zsh": "terminal"
-  },
-  "types": {
-    "audio": {
-      "icon": "icons/file_icons/audio.svg"
-    },
-    "code": {
-      "icon": "icons/file_icons/code.svg"
-    },
-    "collapsed_chevron": {
-      "icon": "icons/file_icons/chevron_right.svg"
-    },
-    "collapsed_folder": {
-      "icon": "icons/file_icons/folder.svg"
-    },
-    "default": {
-      "icon": "icons/file_icons/file.svg"
-    },
-    "document": {
-      "icon": "icons/file_icons/book.svg"
-    },
-    "eslint": {
-      "icon": "icons/file_icons/eslint.svg"
-    },
-    "expanded_chevron": {
-      "icon": "icons/file_icons/chevron_down.svg"
-    },
-    "expanded_folder": {
-      "icon": "icons/file_icons/folder_open.svg"
-    },
-    "image": {
-      "icon": "icons/file_icons/image.svg"
-    },
-    "lock": {
-      "icon": "icons/file_icons/lock.svg"
-    },
-    "log": {
-      "icon": "icons/file_icons/info.svg"
-    },
-    "prettier": {
-      "icon": "icons/file_icons/prettier.svg"
-    },
-    "rust": {
-      "icon": "icons/file_icons/rust.svg"
-    },
-    "settings": {
-      "icon": "icons/file_icons/settings.svg"
-    },
-    "storage": {
-      "icon": "icons/file_icons/database.svg"
-    },
-    "template": {
-      "icon": "icons/file_icons/html.svg"
-    },
-    "terminal": {
-      "icon": "icons/file_icons/terminal.svg"
-    },
-    "toml": {
-      "icon": "icons/file_icons/toml.svg"
-    },
-    "typescript": {
-      "icon": "icons/file_icons/typescript.svg"
-    },
-    "vcs": {
-      "icon": "icons/file_icons/git.svg"
-    },
-    "video": {
-      "icon": "icons/file_icons/video.svg"
+    "suffixes": {
+        "aac": "audio",
+        "accdb": "storage",
+        "bak": "backup",
+        "bash": "terminal",
+        "bash_aliases": "terminal",
+        "bash_logout": "terminal",
+        "bash_profile": "terminal",
+        "bashrc": "terminal",
+        "bmp": "image",
+        "c": "code",
+        "cc": "code",
+        "conf": "settings",
+        "cpp": "code",
+        "css": "code",
+        "csv": "storage",
+        "dat": "storage",
+        "db": "storage",
+        "dbf": "storage",
+        "dll": "storage",
+        "doc": "document",
+        "docx": "document",
+        "eslintrc": "eslint",
+        "eslintrc.js": "eslint",
+        "eslintrc.json": "eslint",
+        "fmp": "storage",
+        "fp7": "storage",
+        "flac": "audio",
+        "fish": "terminal",
+        "frm": "storage",
+        "gdb": "storage",
+        "gitattributes": "vcs",
+        "gitignore": "vcs",
+        "gitmodules": "vcs",
+        "gif": "image",
+        "go": "code",
+        "h": "code",
+        "handlebars": "code",
+        "hbs": "template",
+        "htm": "template",
+        "html": "template",
+        "ib": "storage",
+        "ico": "image",
+        "ini": "settings",
+        "java": "code",
+        "jpeg": "image",
+        "jpg": "image",
+        "js": "code",
+        "json": "storage",
+        "ldf": "storage",
+        "lock": "lock",
+        "log": "log",
+        "mdb": "storage",
+        "md": "document",
+        "mdf": "storage",
+        "mdx": "document",
+        "mp3": "audio",
+        "mp4": "video",
+        "myd": "storage",
+        "myi": "storage",
+        "ods": "document",
+        "odp": "document",
+        "odt": "document",
+        "ogg": "video",
+        "pdb": "storage",
+        "pdf": "document",
+        "php": "code",
+        "png": "image",
+        "ppt": "document",
+        "pptx": "document",
+        "prettierignore": "prettier",
+        "prettierrc": "prettier",
+        "profile": "terminal",
+        "ps1": "terminal",
+        "psd": "image",
+        "py": "code",
+        "rb": "code",
+        "rkt": "code",
+        "rs": "rust",
+        "rtf": "document",
+        "sav": "storage",
+        "scm": "code",
+        "sh": "terminal",
+        "sqlite": "storage",
+        "sdf": "storage",
+        "svelte": "template",
+        "svg": "image",
+        "swift": "code",
+        "ts": "typescript",
+        "tsx": "code",
+        "tiff": "image",
+        "toml": "toml",
+        "tsv": "storage",
+        "txt": "document",
+        "wav": "audio",
+        "webm": "video",
+        "xls": "document",
+        "xlsx": "document",
+        "xml": "template",
+        "yaml": "settings",
+        "yml": "settings",
+        "zlogin": "terminal",
+        "zsh": "terminal",
+        "zsh_aliases": "terminal",
+        "zshenv": "terminal",
+        "zsh_histfile": "terminal",
+        "zsh_profile": "terminal",
+        "zshrc": "terminal"
+    },
+    "types": {
+        "audio": {
+            "icon": "icons/file_icons/audio.svg"
+        },
+        "code": {
+            "icon": "icons/file_icons/code.svg"
+        },
+        "collapsed_chevron": {
+            "icon": "icons/file_icons/chevron_right.svg"
+        },
+        "collapsed_folder": {
+            "icon": "icons/file_icons/folder.svg"
+        },
+        "default": {
+            "icon": "icons/file_icons/file.svg"
+        },
+        "document": {
+            "icon": "icons/file_icons/book.svg"
+        },
+        "eslint": {
+            "icon": "icons/file_icons/eslint.svg"
+        },
+        "expanded_chevron": {
+            "icon": "icons/file_icons/chevron_down.svg"
+        },
+        "expanded_folder": {
+            "icon": "icons/file_icons/folder_open.svg"
+        },
+        "image": {
+            "icon": "icons/file_icons/image.svg"
+        },
+        "lock": {
+            "icon": "icons/file_icons/lock.svg"
+        },
+        "log": {
+            "icon": "icons/file_icons/info.svg"
+        },
+        "prettier": {
+            "icon": "icons/file_icons/prettier.svg"
+        },
+        "rust": {
+            "icon": "icons/file_icons/rust.svg"
+        },
+        "settings": {
+            "icon": "icons/file_icons/settings.svg"
+        },
+        "storage": {
+            "icon": "icons/file_icons/database.svg"
+        },
+        "template": {
+            "icon": "icons/file_icons/html.svg"
+        },
+        "terminal": {
+            "icon": "icons/file_icons/terminal.svg"
+        },
+        "toml": {
+            "icon": "icons/file_icons/toml.svg"
+        },
+        "typescript": {
+            "icon": "icons/file_icons/typescript.svg"
+        },
+        "vcs": {
+            "icon": "icons/file_icons/git.svg"
+        },
+        "video": {
+            "icon": "icons/file_icons/video.svg"
+        }
     }
-  }
 }

assets/icons/file_icons/folder.svg 🔗

@@ -1,5 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 5.125C2 4.84886 2.22386 4.625 2.5 4.625H11.5C11.7761 4.625 12 4.84886 12 5.125V11.125C12 11.4011 11.7761 11.625 11.5 11.625H2.5C2.22386 11.625 2 11.4011 2 11.125V5.125Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M6.38197 2.375H2.5C2.22386 2.375 2 2.59886 2 2.875V4.375H8L7.27639 2.92779C7.107 2.589 6.76074 2.375 6.38197 2.375Z" fill="black"/>
-<path d="M2 8V4.375M2 4.375V2.875C2 2.59886 2.22386 2.375 2.5 2.375H6.38197C6.76074 2.375 7.107 2.589 7.27639 2.92779L8 4.375H2Z" stroke="black" stroke-width="1.25"/>

assets/icons/file_icons/folder_open.svg 🔗

@@ -1,5 +1,4 @@
 <svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 2.53125H2.21875V10.625L4.5 4.59375H7.96875L7 2.53125Z" fill="black"/>
-<path d="M4.47293 4.94363C4.54554 4.74743 4.73263 4.61719 4.94184 4.61719H12.8755C13.2237 4.61719 13.4653 4.9642 13.3445 5.29074L11.1208 11.2986C11.0482 11.4948 10.8611 11.625 10.6519 11.625H2.71821C2.37002 11.625 2.12844 11.278 2.2493 10.9514L4.47293 4.94363Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
-<path d="M8 4.4024L7.27505 2.93264C7.10664 2.59119 6.75894 2.375 6.37821 2.375H2.5C2.22386 2.375 2 2.59886 2 2.875V11.125C2 11.4011 2.22386 11.625 2.5 11.625H4.00781" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M3.49165 6.13802C3.4991 5.86198 3.72386 5.64062 4 5.64062H13C13.2761 5.64062 13.4991 5.86198 13.4916 6.13802C13.4529 7.57407 13.2341 11.625 12 11.625H2C3.23412 11.625 3.45287 7.57407 3.49165 6.13802Z" fill="black" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M4.00781 11.625H2.42841C2.18186 11.625 1.97212 11.4453 1.93432 11.2017L0.651964 2.93603C0.604944 2.63296 0.839355 2.35938 1.14605 2.35938H4.6164C4.95332 2.35938 5.26759 2.52904 5.45244 2.81072L5.8125 3.35938H8.89008C9.37767 3.35938 9.79418 3.71103 9.87593 4.19171L10.125 5.65625" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/git.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <circle cx="4" cy="10" r="2" stroke="black" stroke-width="1.25"/>
-<circle cx="10" cy="4" r="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+<circle cx="10" cy="4" r="2" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
 <line x1="3.625" y1="2.625" x2="3.625" y2="7.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M10 6V6C10 8.20914 8.20914 10 6 10V10" stroke="black" stroke-width="1.25"/>
 </svg>

assets/icons/file_icons/image.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M6.5 3C6.91421 3 7.25 2.66421 7.25 2.25C7.25 1.83579 6.91421 1.5 6.5 1.5C6.08579 1.5 5.75 1.83579 5.75 2.25C5.75 2.66421 6.08579 3 6.5 3Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8L9 5L12 8H6Z" fill="black" fill-opacity="0.33"/>
+<path d="M6 8L9 5L12 8H6Z" fill="black" fill-opacity="0.3"/>
 <path d="M2 10L5 7L7.375 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M6 8L7.5 6.5L9 5L10.5 6.5L12 8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M3.375 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H7.35938M9.64062 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10.125" stroke="black" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/file_icons/lock.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/>
-<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.66" stroke-width="1.25"/>
+<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.6" stroke-width="1.25"/>
 <circle cx="7" cy="8" r="1" fill="black"/>
 <path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/magnifying_glass.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 12L9.41379 9.41379M2 6.31034C2 3.92981 3.92981 2 6.31034 2C8.6909 2 10.6207 3.92981 10.6207 6.31034C10.6207 8.6909 8.6909 10.6207 6.31034 10.6207C3.92981 10.6207 2 8.6909 2 6.31034Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/notebook.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.03125 2.96875C2.03125 2.41647 2.47897 1.96875 3.03125 1.96875H5V12H3.03125C2.47897 12 2.03125 11.5523 2.03125 11V2.96875Z" fill="black" fill-opacity="0.33"/>
+<path d="M2.03125 2.96875C2.03125 2.41647 2.47897 1.96875 3.03125 1.96875H5V12H3.03125C2.47897 12 2.03125 11.5523 2.03125 11V2.96875Z" fill="black" fill-opacity="0.3"/>
 <rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/>
 <path d="M9.5 5L7.5 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 7H7.5" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 9H7.5" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 7H7.5" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 9H7.5" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/package.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M1.62677 3.88472L6.99983 6.78517M1.62677 3.88472L1.63137 9.90006L7.00442 12.8005M1.62677 3.88472L4.31117 2.54211M6.99983 6.78517L7.00442 12.8005M6.99983 6.78517L9.68414 5.33084M7.00442 12.8005L12.373 9.89186L12.3684 3.87652M4.31117 2.54211L6.99556 1.1995L12.3684 3.87652M4.31117 2.54211L9.68414 5.33084M12.3684 3.87652L9.68414 5.33084" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.03125 12.5625V6.78125L1.5625 3.9375V9.75L7.03125 12.5625Z" fill="black" fill-opacity="0.33"/>
+<path d="M7.03125 12.5625V6.78125L1.5625 3.9375V9.75L7.03125 12.5625Z" fill="black" fill-opacity="0.3"/>
 </svg>

assets/icons/file_icons/plus.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 3V11M11 7H3" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/prettier.svg 🔗

@@ -1,12 +1,12 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M2 2.86328H8.51563" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M11 2.86328L12 2.86328" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M11 2.86328L12 2.86328" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M4.79688 5.6263L7.15625 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M7.15625 8.3737L12 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M2 11.1094H3.54687" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M5.97656 11.1094H8.35938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/project.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.03125 2V2.03125M2.03125 8C2.03125 10 5 10 5 10M2.03125 8V2.03125M2.03125 8L2.03125 11M2.03125 2.03125C2.03125 4 5 4 5 4" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="7.375" y="2.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+<rect x="7.375" y="8.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/file_icons/replace.svg 🔗

@@ -0,0 +1,11 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 12C4.97279 12 3.22735 10.7936 2.4425 9.0595M7 2C9.11228 2 10.9186 3.30981 11.6512 5.16152" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="1.65625" cy="1.67188" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="3.71094" cy="1.67188" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="4.96094" cy="3.36719" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="3.71094" cy="4.79688" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="4.60156" cy="6.67188" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="1.65625" cy="4.17188" r="0.625" fill="black" fill-opacity="0.5"/>
+<circle cx="1.65625" cy="6.67188" r="0.625" fill="black" fill-opacity="0.5"/>

assets/icons/file_icons/replace_all.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.10517 5.8012C4.07193 5.73172 4.00176 5.6875 3.92475 5.6875H3.44609C3.33564 5.6875 3.24609 5.77704 3.24609 5.8875V7.26172C3.24609 7.53786 3.02224 7.76172 2.74609 7.76172H2.64062C2.36448 7.76172 2.14062 7.53786 2.14062 7.26172V2.625C2.14062 2.34886 2.36448 2.125 2.64062 2.125H4.1875C5.41406 2.125 6.16406 2.80469 6.16406 3.92188C6.16406 4.57081 5.85885 5.12418 5.36073 5.40943C5.25888 5.46775 5.20921 5.59421 5.2617 5.69918L5.93117 7.03811C6.09739 7.37056 5.85564 7.76172 5.48395 7.76172H5.35806C5.16552 7.76172 4.99009 7.65117 4.907 7.47748L4.10517 5.8012ZM3.44609 3.03125C3.33564 3.03125 3.24609 3.12079 3.24609 3.23125V4.63594C3.24609 4.74639 3.33564 4.83594 3.44609 4.83594H4.03125C4.66016 4.83594 5.03516 4.49609 5.03516 3.92578C5.03516 3.36719 4.66797 3.03125 4.04297 3.03125H3.44609Z" fill="black" fill-opacity="0.5"/>

assets/icons/file_icons/replace_next.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.96454 5.6762C3.93131 5.60672 3.86114 5.5625 3.78412 5.5625H3.30547C3.19501 5.5625 3.10547 5.65204 3.10547 5.7625V7.13672C3.10547 7.41286 2.88161 7.63672 2.60547 7.63672H2.5C2.22386 7.63672 2 7.41286 2 7.13672V2.5C2 2.22386 2.22386 2 2.5 2H4.04688C5.27344 2 6.02344 2.67969 6.02344 3.79688C6.02344 4.44581 5.71823 4.99918 5.2201 5.28443C5.11826 5.34275 5.06859 5.46921 5.12107 5.57418L5.79054 6.91311C5.95677 7.24556 5.71502 7.63672 5.34333 7.63672H5.21743C5.02489 7.63672 4.84946 7.52617 4.76638 7.35248L3.96454 5.6762ZM3.30547 2.90625C3.19501 2.90625 3.10547 2.99579 3.10547 3.10625V4.51094C3.10547 4.62139 3.19501 4.71094 3.30547 4.71094H3.89062C4.51953 4.71094 4.89453 4.37109 4.89453 3.80078C4.89453 3.24219 4.52734 2.90625 3.90234 2.90625H3.30547Z" fill="black" fill-opacity="0.5"/>
+<path d="M3.78412 5.6125C3.84188 5.6125 3.89451 5.64567 3.91944 5.69777L4.72127 7.37405C4.81266 7.56511 5.00564 7.68672 5.21743 7.68672H5.34333C5.75219 7.68672 6.01811 7.25645 5.83526 6.89075L5.1658 5.55182C5.12715 5.47453 5.16207 5.37528 5.24495 5.32782C5.76044 5.03262 6.07344 4.46155 6.07344 3.79688C6.07344 3.22658 5.88164 2.76303 5.52873 2.44248C5.17642 2.12247 4.6691 1.95 4.04688 1.95H2.5C2.19624 1.95 1.95 2.19624 1.95 2.5V7.13672C1.95 7.44048 2.19624 7.68672 2.5 7.68672H2.60547C2.90923 7.68672 3.15547 7.44048 3.15547 7.13672V5.7625C3.15547 5.67966 3.22263 5.6125 3.30547 5.6125H3.78412ZM3.15547 3.10625C3.15547 3.02341 3.22263 2.95625 3.30547 2.95625H3.90234C4.20626 2.95625 4.44101 3.03787 4.59926 3.18111C4.75686 3.32376 4.84453 3.5329 4.84453 3.80078C4.84453 4.07452 4.75491 4.28758 4.59484 4.43268C4.43413 4.57837 4.19643 4.66094 3.89062 4.66094H3.30547C3.22263 4.66094 3.15547 4.59378 3.15547 4.51094V3.10625Z" stroke="black" stroke-opacity="0.5" stroke-width="0.1"/>
+<path d="M7.5 5.88672C9.433 5.88672 11 7.45372 11 9.38672V12M11 12L13 10M11 12L9 10" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/rust.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.27935 9.98207C4.32063 9.4038 3.9204 8.89049 3.35998 8.80276L2.60081 8.68387C2.37979 8.64945 2.20167 8.48001 2.15225 8.25614L2.01378 7.63511C1.96382 7.41235 2.05233 7.1807 2.23696 7.05125L2.8631 6.61242C3.33337 6.28297 3.47456 5.6369 3.18621 5.13364L2.79467 4.45092C2.68118 4.25261 2.69801 4.00374 2.83757 3.82321L3.22314 3.32436C3.3627 3.14438 3.59621 3.06994 3.81071 3.13772L4.57531 3.37769C5.11944 3.54879 5.70048 3.26159 5.90683 2.71886L6.1811 1.99782C6.26255 1.78395 6.46345 1.64285 6.68772 1.6423L7.31007 1.64063C7.53434 1.64007 7.73579 1.78006 7.81834 1.99337L8.09965 2.72275C8.30821 3.26214 8.88655 3.54712 9.42903 3.37714L10.1632 3.14716C10.3772 3.07994 10.6096 3.15382 10.7492 3.3327L11.1374 3.83099" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_icons/toml.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M5 5H9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
 <path d="M7 5L7 10" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M4 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H4M10 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M4 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H4M10 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/typescript.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 4.375V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H3.375" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M12 4.375V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H3.375" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/file_icons/video.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M1.65625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V9.34375M12.3438 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V4.65625" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M9 7.01562L5.65624 9.3125L5.65624 4.6875L9 7.01562Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 7.01562L5.65624 9.3125L5.65624 4.6875L9 7.01562Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/keymaps/default.json 🔗

@@ -22,6 +22,7 @@
       "alt-cmd-right": "pane::ActivateNextItem",
       "cmd-w": "pane::CloseActiveItem",
       "alt-cmd-t": "pane::CloseInactiveItems",
+      "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
       "cmd-k u": "pane::CloseCleanItems",
       "cmd-k cmd-w": "pane::CloseAllItems",
       "cmd-shift-w": "workspace::CloseWindow",
@@ -226,12 +227,26 @@
       "alt-enter": "search::SelectAllMatches"
     }
   },
+  {
+    "context": "BufferSearchBar > Editor",
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
   {
     "context": "ProjectSearchBar",
     "bindings": {
       "escape": "project_search::ToggleFocus"
     }
   },
+  {
+    "context": "ProjectSearchBar > Editor",
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
   {
     "context": "ProjectSearchView",
     "bindings": {
@@ -411,7 +426,6 @@
       "cmd-k cmd-t": "theme_selector::Toggle",
       "cmd-k cmd-s": "zed::OpenKeymap",
       "cmd-t": "project_symbols::Toggle",
-      "cmd-ctrl-t": "semantic_search::Toggle",
       "cmd-p": "file_finder::Toggle",
       "cmd-shift-p": "command_palette::Toggle",
       "cmd-shift-m": "diagnostics::Deploy",

assets/settings/default.json 🔗

@@ -324,8 +324,8 @@
     // the terminal will default to matching the buffer's font family.
     // "font_family": "Zed Mono"
   },
-  // Difference settings for vector_store
-  "vector_store": {
+  // Difference settings for semantic_index
+  "semantic_index": {
     "enabled": false,
     "reindexing_delay_seconds": 600
   },

crates/ai/src/assistant.rs 🔗

@@ -1637,6 +1637,7 @@ impl ConversationEditor {
             let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_gutter(false, cx);
+            editor.set_show_wrap_guides(false, cx);
             editor
         });
 

crates/collab/src/tests.rs 🔗

@@ -12,10 +12,7 @@ use client::{
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
-use gpui::{
-    elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
-    ViewContext, ViewHandle, WeakViewHandle,
-};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle};
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
@@ -466,42 +463,8 @@ impl TestClient {
         &self,
         project: &ModelHandle<Project>,
         cx: &mut TestAppContext,
-    ) -> ViewHandle<Workspace> {
-        struct WorkspaceContainer {
-            workspace: Option<WeakViewHandle<Workspace>>,
-        }
-
-        impl Entity for WorkspaceContainer {
-            type Event = ();
-        }
-
-        impl View for WorkspaceContainer {
-            fn ui_name() -> &'static str {
-                "WorkspaceContainer"
-            }
-
-            fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-                if let Some(workspace) = self
-                    .workspace
-                    .as_ref()
-                    .and_then(|workspace| workspace.upgrade(cx))
-                {
-                    ChildView::new(&workspace, cx).into_any()
-                } else {
-                    Empty::new().into_any()
-                }
-            }
-        }
-
-        // We use a workspace container so that we don't need to remove the window in order to
-        // drop the workspace and we can use a ViewHandle instead.
-        let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
-        let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
-        container.update(cx, |container, cx| {
-            container.workspace = Some(workspace.downgrade());
-            cx.notify();
-        });
-        workspace
+    ) -> WindowHandle<Workspace> {
+        cx.add_window(|cx| Workspace::test_new(project.clone(), cx))
     }
 }
 

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

@@ -7,8 +7,7 @@ use client::{User, RECEIVE_TIMEOUT};
 use collections::HashSet;
 use editor::{
     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
-    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
-    Undo,
+    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
 };
 use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
 use futures::StreamExt as _;
@@ -1208,7 +1207,7 @@ async fn test_share_project(
     cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
+    let window_b = cx_b.add_window(|_| EmptyView);
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -1316,7 +1315,7 @@ async fn test_share_project(
         .await
         .unwrap();
 
-    let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
 
     // Client A sees client B's selection
     deterministic.run_until_parked();
@@ -1499,8 +1498,8 @@ async fn test_host_disconnect(
     deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (window_id_b, workspace_b) =
-        cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -1509,11 +1508,9 @@ async fn test_host_disconnect(
         .unwrap()
         .downcast::<Editor>()
         .unwrap();
-    assert!(cx_b
-        .read_window(window_id_b, |cx| editor_b.is_focused(cx))
-        .unwrap());
+    assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
     editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
-    assert!(cx_b.is_window_edited(workspace_b.window_id()));
+    assert!(window_b.is_edited(cx_b));
 
     // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
     server.forbid_connections();
@@ -1525,10 +1522,10 @@ async fn test_host_disconnect(
     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
 
     // Ensure client B's edited state is reset and that the whole window is blurred.
-    cx_b.read_window(window_id_b, |cx| {
+    window_b.read_with(cx_b, |cx| {
         assert_eq!(cx.focused_view_id(), None);
     });
-    assert!(!cx_b.is_window_edited(workspace_b.window_id()));
+    assert!(!window_b.is_edited(cx_b));
 
     // Ensure client B is not prompted to save edits when closing window after disconnecting.
     let can_close = workspace_b
@@ -3445,13 +3442,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let (window_a, _) = cx_a.add_window(|_| EmptyView);
-    let editor_a = cx_a.add_view(window_a, |cx| {
-        Editor::for_buffer(buffer_a, Some(project_a), cx)
-    });
+    let window_a = cx_a.add_window(|_| EmptyView);
+    let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
     let mut editor_cx_a = EditorTestContext {
         cx: cx_a,
-        window_id: window_a,
+        window: window_a.into(),
         editor: editor_a,
     };
 
@@ -3460,13 +3455,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
-        Editor::for_buffer(buffer_b, Some(project_b), cx)
-    });
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
     let mut editor_cx_b = EditorTestContext {
         cx: cx_b,
-        window_id: window_b,
+        window: window_b.into(),
         editor: editor_b,
     };
 
@@ -4205,8 +4198,8 @@ async fn test_collaborating_with_completion(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| {
         Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
     });
 
@@ -5316,7 +5309,8 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -5540,7 +5534,8 @@ async fn test_collaborating_with_renames(
         .unwrap();
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -5571,6 +5566,7 @@ async fn test_collaborating_with_renames(
         .unwrap();
     prepare_rename.await.unwrap();
     editor_b.update(cx_b, |editor, cx| {
+        use editor::ToOffset;
         let rename = editor.pending_rename().unwrap();
         let buffer = editor.buffer().read(cx).snapshot(cx);
         assert_eq!(
@@ -6445,8 +6441,10 @@ async fn test_basic_following(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let window_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = window_a.root(cx_a);
+    let window_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = window_b.root(cx_b);
 
     // Client A opens some editors.
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
@@ -6529,7 +6527,8 @@ async fn test_basic_following(
     cx_c.foreground().run_until_parked();
     let active_call_c = cx_c.read(ActiveCall::global);
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
-    let workspace_c = client_c.build_workspace(&project_c, cx_c);
+    let window_c = client_c.build_workspace(&project_c, cx_c);
+    let workspace_c = window_c.root(cx_c);
     active_call_c
         .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
         .await
@@ -6547,7 +6546,7 @@ async fn test_basic_following(
     cx_d.foreground().run_until_parked();
     let active_call_d = cx_d.read(ActiveCall::global);
     let project_d = client_d.build_remote_project(project_id, cx_d).await;
-    let workspace_d = client_d.build_workspace(&project_d, cx_d);
+    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
     active_call_d
         .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
         .await
@@ -6645,6 +6644,7 @@ async fn test_basic_following(
     }
 
     // Client C closes the project.
+    window_c.remove(cx_c);
     cx_c.drop_last(workspace_c);
 
     // Clients A and B see that client B is following A, and client C is not present in the followers.
@@ -6874,9 +6874,7 @@ async fn test_basic_following(
     });
 
     // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-    let panel = cx_b.add_view(workspace_b.window_id(), |_| {
-        TestPanel::new(DockPosition::Left)
-    });
+    let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
     workspace_b.update(cx_b, |workspace, cx| {
         workspace.add_panel(panel, cx);
         workspace.toggle_panel_focus::<TestPanel>(cx);
@@ -6904,7 +6902,7 @@ async fn test_basic_following(
 
     // Client B activates an item that doesn't implement following,
     // so the previously-opened screen-sharing item gets activated.
-    let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new());
+    let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
     workspace_b.update(cx_b, |workspace, cx| {
         workspace.active_pane().update(cx, |pane, cx| {
             pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@@ -7066,10 +7064,10 @@ async fn test_following_tab_order(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
 
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
 
     let client_b_id = project_a.read_with(cx_a, |project, _| {
@@ -7192,7 +7190,7 @@ async fn test_peers_following_each_other(
         .unwrap();
 
     // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
@@ -7204,7 +7202,7 @@ async fn test_peers_following_each_other(
         .unwrap();
 
     // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
     let _editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
@@ -7363,7 +7361,7 @@ async fn test_auto_unfollowing(
         .unwrap();
 
     // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -7374,7 +7372,7 @@ async fn test_auto_unfollowing(
         .unwrap();
 
     // Client B starts following client A.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
     let leader_id = project_b.read_with(cx_b, |project, _| {
         project.collaborators().values().next().unwrap().peer_id
@@ -7502,14 +7500,14 @@ async fn test_peers_simultaneously_following_each_other(
 
     client_a.fs.insert_tree("/a", json!({})).await;
     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let project_id = active_call_a
         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
         .await
         .unwrap();
 
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 
     deterministic.run_until_parked();
     let client_a_id = project_b.read_with(cx_b, |project, _| {
@@ -7601,8 +7599,8 @@ async fn test_on_input_format_from_host_to_guest(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_a, _) = cx_a.add_window(|_| EmptyView);
-    let editor_a = cx_a.add_view(window_a, |cx| {
+    let window_a = cx_a.add_window(|_| EmptyView);
+    let editor_a = window_a.add_view(cx_a, |cx| {
         Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
     });
 
@@ -7730,8 +7728,8 @@ async fn test_on_input_format_from_guest_to_host(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| {
         Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
     });
 
@@ -7891,7 +7889,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     cx_a.foreground().start_waiting();
 
     let _buffer_a = project_a
@@ -7959,7 +7957,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Host editor update the cache version after every cache/view change",
         );
     });
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -8198,8 +8196,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     cx_a.foreground().start_waiting();
     cx_b.foreground().start_waiting();
 

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -374,7 +374,7 @@ impl CollabTitlebarItem {
                         "Share Feedback",
                         feedback::feedback_editor::GiveFeedback,
                     ),
-                    ContextMenuItem::action("Sign out", SignOut),
+                    ContextMenuItem::action("Sign Out", SignOut),
                 ]
             } else {
                 vec![

crates/collab_ui/src/contact_list.rs 🔗

@@ -305,18 +305,18 @@ impl ContactList {
             github_login
         );
         let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.spawn(|_, mut cx| async move {
             if answer.next().await == Some(0) {
                 if let Err(e) = user_store
                     .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
                     .await
                 {
-                    cx.prompt(
-                        window_id,
+                    window.prompt(
                         PromptLevel::Info,
                         &format!("Failed to remove contact: {}", e),
                         &["Ok"],
+                        &mut cx,
                     );
                 }
             }

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
-    AnyElement, AppContext, Entity, View, ViewContext,
+    AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
 };
 use util::ResultExt;
 use workspace::AppState;
@@ -16,10 +16,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     let app_state = Arc::downgrade(app_state);
     let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
     cx.spawn(|mut cx| async move {
-        let mut notification_windows = Vec::new();
+        let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
         while let Some(incoming_call) = incoming_call.next().await {
-            for window_id in notification_windows.drain(..) {
-                cx.remove_window(window_id);
+            for window in notification_windows.drain(..) {
+                window.remove(&mut cx);
             }
 
             if let Some(incoming_call) = incoming_call {
@@ -31,7 +31,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 
                 for screen in cx.platform().screens() {
                     let screen_bounds = screen.bounds();
-                    let (window_id, _) = cx.add_window(
+                    let window = cx.add_window(
                         WindowOptions {
                             bounds: WindowBounds::Fixed(RectF::new(
                                 screen_bounds.upper_right()
@@ -49,7 +49,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                         |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
                     );
 
-                    notification_windows.push(window_id);
+                    notification_windows.push(window);
                 }
             }
         }

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 
             for screen in cx.platform().screens() {
                 let screen_bounds = screen.bounds();
-                let (window_id, _) = cx.add_window(
+                let window = cx.add_window(
                     WindowOptions {
                         bounds: WindowBounds::Fixed(RectF::new(
                             screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
@@ -52,20 +52,20 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                 notification_windows
                     .entry(*project_id)
                     .or_insert(Vec::new())
-                    .push(window_id);
+                    .push(window);
             }
         }
         room::Event::RemoteProjectUnshared { project_id } => {
-            if let Some(window_ids) = notification_windows.remove(&project_id) {
-                for window_id in window_ids {
-                    cx.update_window(window_id, |cx| cx.remove_window());
+            if let Some(windows) = notification_windows.remove(&project_id) {
+                for window in windows {
+                    window.remove(cx);
                 }
             }
         }
         room::Event::Left => {
-            for (_, window_ids) in notification_windows.drain() {
-                for window_id in window_ids {
-                    cx.update_window(window_id, |cx| cx.remove_window());
+            for (_, windows) in notification_windows.drain() {
+                for window in windows {
+                    window.remove(cx);
                 }
             }
         }

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -20,11 +20,11 @@ pub fn init(cx: &mut AppContext) {
                 {
                     status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
                 }
-            } else if let Some((window_id, _)) = status_indicator.take() {
-                cx.update_window(window_id, |cx| cx.remove_window());
+            } else if let Some(window) = status_indicator.take() {
+                window.update(cx, |cx| cx.remove_window());
             }
-        } else if let Some((window_id, _)) = status_indicator.take() {
-            cx.update_window(window_id, |cx| cx.remove_window());
+        } else if let Some(window) = status_indicator.take() {
+            window.update(cx, |cx| cx.remove_window());
         }
     })
     .detach();

crates/command_palette/src/command_palette.rs 🔗

@@ -1,8 +1,8 @@
 use collections::CommandPaletteFilter;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
-    ViewContext,
+    actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle,
+    AppContext, Element, MouseState, ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::cmp;
@@ -28,7 +28,7 @@ pub struct CommandPaletteDelegate {
 pub enum Event {
     Dismissed,
     Confirmed {
-        window_id: usize,
+        window: AnyWindowHandle,
         focused_view_id: usize,
         action: Box<dyn Action>,
     },
@@ -80,12 +80,13 @@ impl PickerDelegate for CommandPaletteDelegate {
         query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
-        let window_id = cx.window_id();
         let view_id = self.focused_view_id;
+        let window = cx.window();
         cx.spawn(move |picker, mut cx| async move {
-            let actions = cx
-                .available_actions(window_id, view_id)
+            let actions = window
+                .available_actions(view_id, &cx)
                 .into_iter()
+                .flatten()
                 .filter_map(|(name, action, bindings)| {
                     let filtered = cx.read(|cx| {
                         if cx.has_global::<CommandPaletteFilter>() {
@@ -162,13 +163,15 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if !self.matches.is_empty() {
-            let window_id = cx.window_id();
+            let window = cx.window();
             let focused_view_id = self.focused_view_id;
             let action_ix = self.matches[self.selected_ix].candidate_id;
             let action = self.actions.remove(action_ix).action;
             cx.app_context()
                 .spawn(move |mut cx| async move {
-                    cx.dispatch_action(window_id, focused_view_id, action.as_ref())
+                    window
+                        .dispatch_action(focused_view_id, action.as_ref(), &mut cx)
+                        .ok_or_else(|| anyhow!("window was closed"))
                 })
                 .detach_and_log_err(cx);
         }
@@ -295,8 +298,9 @@ mod tests {
         let app_state = init_test(cx);
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let editor = cx.add_view(window_id, |cx| {
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+        let editor = window.add_view(cx, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);
             editor

crates/context_menu/src/context_menu.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{
-    anyhow,
+    anyhow::{self, anyhow},
     elements::*,
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
@@ -218,12 +218,14 @@ impl ContextMenu {
             if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
                 match action {
                     ContextMenuItemAction::Action(action) => {
-                        let window_id = cx.window_id();
+                        let window = cx.window();
                         let view_id = self.parent_view_id;
                         let action = action.boxed_clone();
                         cx.app_context()
                             .spawn(|mut cx| async move {
-                                cx.dispatch_action(window_id, view_id, action.as_ref())
+                                window
+                                    .dispatch_action(view_id, action.as_ref(), &mut cx)
+                                    .ok_or_else(|| anyhow!("window was closed"))
                             })
                             .detach_and_log_err(cx);
                     }
@@ -480,17 +482,19 @@ impl ContextMenu {
                             .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
                             .on_click(MouseButton::Left, move |_, menu, cx| {
                                 menu.cancel(&Default::default(), cx);
-                                let window_id = cx.window_id();
+                                let window = cx.window();
                                 match &action {
                                     ContextMenuItemAction::Action(action) => {
                                         let action = action.boxed_clone();
                                         cx.app_context()
                                             .spawn(|mut cx| async move {
-                                                cx.dispatch_action(
-                                                    window_id,
-                                                    view_id,
-                                                    action.as_ref(),
-                                                )
+                                                window
+                                                    .dispatch_action(
+                                                        view_id,
+                                                        action.as_ref(),
+                                                        &mut cx,
+                                                    )
+                                                    .ok_or_else(|| anyhow!("window was closed"))
                                             })
                                             .detach_and_log_err(cx);
                                     }

crates/copilot/src/copilot.rs 🔗

@@ -338,9 +338,9 @@ impl Copilot {
         let (server, fake_server) =
             LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
         let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
-        let this = cx.add_model(|cx| Self {
+        let this = cx.add_model(|_| Self {
             http: http.clone(),
-            node_runtime: NodeRuntime::instance(http, cx.background().clone()),
+            node_runtime: NodeRuntime::instance(http),
             server: CopilotServer::Running(RunningCopilotServer {
                 lsp: Arc::new(server),
                 sign_in_status: SignInStatus::Authorized,

crates/copilot/src/sign_in.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     geometry::rect::RectF,
     platform::{WindowBounds, WindowKind, WindowOptions},
     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
-    ViewHandle,
+    WindowHandle,
 };
 use theme::ui::modal;
 
@@ -18,43 +18,43 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 
 pub fn init(cx: &mut AppContext) {
     if let Some(copilot) = Copilot::global(cx) {
-        let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
+        let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
         cx.observe(&copilot, move |copilot, cx| {
             let status = copilot.read(cx).status();
 
             match &status {
                 crate::Status::SigningIn { prompt } => {
-                    if let Some(code_verification_handle) = code_verification.as_mut() {
-                        let window_id = code_verification_handle.window_id();
-                        let updated = cx.update_window(window_id, |cx| {
-                            code_verification_handle.update(cx, |code_verification, cx| {
-                                code_verification.set_status(status.clone(), cx)
-                            });
-                            cx.activate_window();
-                        });
-                        if updated.is_none() {
-                            code_verification = Some(create_copilot_auth_window(cx, &status));
+                    if let Some(window) = verification_window.as_mut() {
+                        let updated = window
+                            .root(cx)
+                            .map(|root| {
+                                root.update(cx, |verification, cx| {
+                                    verification.set_status(status.clone(), cx);
+                                    cx.activate_window();
+                                })
+                            })
+                            .is_some();
+                        if !updated {
+                            verification_window = Some(create_copilot_auth_window(cx, &status));
                         }
                     } else if let Some(_prompt) = prompt {
-                        code_verification = Some(create_copilot_auth_window(cx, &status));
+                        verification_window = Some(create_copilot_auth_window(cx, &status));
                     }
                 }
                 Status::Authorized | Status::Unauthorized => {
-                    if let Some(code_verification) = code_verification.as_ref() {
-                        let window_id = code_verification.window_id();
-                        cx.update_window(window_id, |cx| {
-                            code_verification.update(cx, |code_verification, cx| {
-                                code_verification.set_status(status, cx)
+                    if let Some(window) = verification_window.as_ref() {
+                        if let Some(verification) = window.root(cx) {
+                            verification.update(cx, |verification, cx| {
+                                verification.set_status(status, cx);
+                                cx.platform().activate(true);
+                                cx.activate_window();
                             });
-
-                            cx.platform().activate(true);
-                            cx.activate_window();
-                        });
+                        }
                     }
                 }
                 _ => {
-                    if let Some(code_verification) = code_verification.take() {
-                        cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
+                    if let Some(code_verification) = verification_window.take() {
+                        code_verification.update(cx, |cx| cx.remove_window());
                     }
                 }
             }
@@ -66,7 +66,7 @@ pub fn init(cx: &mut AppContext) {
 fn create_copilot_auth_window(
     cx: &mut AppContext,
     status: &Status,
-) -> ViewHandle<CopilotCodeVerification> {
+) -> WindowHandle<CopilotCodeVerification> {
     let window_size = theme::current(cx).copilot.modal.dimensions();
     let window_options = WindowOptions {
         bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
@@ -78,10 +78,9 @@ fn create_copilot_auth_window(
         is_movable: true,
         screen: None,
     };
-    let (_, view) = cx.add_window(window_options, |_cx| {
+    cx.add_window(window_options, |_cx| {
         CopilotCodeVerification::new(status.clone())
-    });
-    view
+    })
 }
 
 pub struct CopilotCodeVerification {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -855,7 +855,8 @@ mod tests {
 
         let language_server_id = LanguageServerId(0);
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Create some diagnostics
         project.update(cx, |project, cx| {
@@ -942,7 +943,7 @@ mod tests {
         });
 
         // Open the project diagnostics view while there are already diagnostics.
-        let view = cx.add_view(window_id, |cx| {
+        let view = window.add_view(cx, |cx| {
             ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
         });
 
@@ -1248,9 +1249,10 @@ mod tests {
         let server_id_1 = LanguageServerId(100);
         let server_id_2 = LanguageServerId(101);
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
-        let view = cx.add_view(window_id, |cx| {
+        let view = window.add_view(cx, |cx| {
             ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
         });
 

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     geometry::{rect::RectF, vector::Vector2F},
     platform::{CursorStyle, MouseButton},
     scene::{MouseDown, MouseDrag},
-    AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext,
+    AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
 };
 
 const DEAD_ZONE: f32 = 4.;
@@ -21,7 +21,7 @@ enum State<V: View> {
         region: RectF,
     },
     Dragging {
-        window_id: usize,
+        window: AnyWindowHandle,
         position: Vector2F,
         region_offset: Vector2F,
         region: RectF,
@@ -49,14 +49,14 @@ impl<V: View> Clone for State<V> {
                 region,
             },
             State::Dragging {
-                window_id,
+                window,
                 position,
                 region_offset,
                 region,
                 payload,
                 render,
             } => Self::Dragging {
-                window_id: window_id.clone(),
+                window: window.clone(),
                 position: position.clone(),
                 region_offset: region_offset.clone(),
                 region: region.clone(),
@@ -87,16 +87,16 @@ impl<V: View> DragAndDrop<V> {
         self.containers.insert(handle);
     }
 
-    pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
+    pub fn currently_dragged<T: Any>(&self, window: AnyWindowHandle) -> Option<(Vector2F, Rc<T>)> {
         self.currently_dragged.as_ref().and_then(|state| {
             if let State::Dragging {
                 position,
                 payload,
-                window_id: window_dragged_from,
+                window: window_dragged_from,
                 ..
             } = state
             {
-                if &window_id != window_dragged_from {
+                if &window != window_dragged_from {
                     return None;
                 }
 
@@ -126,9 +126,9 @@ impl<V: View> DragAndDrop<V> {
         cx: &mut WindowContext,
         render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
     ) {
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.update_global(|this: &mut Self, cx| {
-            this.notify_containers_for_window(window_id, cx);
+            this.notify_containers_for_window(window, cx);
 
             match this.currently_dragged.as_ref() {
                 Some(&State::Down {
@@ -141,7 +141,7 @@ impl<V: View> DragAndDrop<V> {
                 }) => {
                     if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
                         this.currently_dragged = Some(State::Dragging {
-                            window_id,
+                            window,
                             region_offset,
                             region,
                             position: event.position,
@@ -163,7 +163,7 @@ impl<V: View> DragAndDrop<V> {
                     ..
                 }) => {
                     this.currently_dragged = Some(State::Dragging {
-                        window_id,
+                        window,
                         region_offset,
                         region,
                         position: event.position,
@@ -188,14 +188,14 @@ impl<V: View> DragAndDrop<V> {
                     State::Down { .. } => None,
                     State::DeadZone { .. } => None,
                     State::Dragging {
-                        window_id,
+                        window,
                         region_offset,
                         position,
                         region,
                         payload,
                         render,
                     } => {
-                        if cx.window_id() != window_id {
+                        if cx.window() != window {
                             return None;
                         }
 
@@ -260,27 +260,27 @@ impl<V: View> DragAndDrop<V> {
 
     pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
         if let Some(State::Dragging {
-            payload, window_id, ..
+            payload, window, ..
         }) = &self.currently_dragged
         {
             if payload.is::<P>() {
-                let window_id = *window_id;
+                let window = *window;
                 self.currently_dragged = Some(State::Canceled);
-                self.notify_containers_for_window(window_id, cx);
+                self.notify_containers_for_window(window, cx);
             }
         }
     }
 
     fn finish_dragging(&mut self, cx: &mut WindowContext) {
-        if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
-            self.notify_containers_for_window(window_id, cx);
+        if let Some(State::Dragging { window, .. }) = self.currently_dragged.take() {
+            self.notify_containers_for_window(window, cx);
         }
     }
 
-    fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) {
+    fn notify_containers_for_window(&mut self, window: AnyWindowHandle, cx: &mut WindowContext) {
         self.containers.retain(|container| {
             if let Some(container) = container.upgrade(cx) {
-                if container.window_id() == window_id {
+                if container.window() == window {
                     container.update(cx, |_, cx| cx.notify());
                 }
                 true

crates/editor/Cargo.toml 🔗

@@ -47,6 +47,7 @@ workspace = { path = "../workspace" }
 
 aho-corasick = "0.7"
 anyhow.workspace = true
+convert_case = "0.6.0"
 futures.workspace = true
 indoc = "1.0.4"
 itertools = "0.10"
@@ -56,12 +57,12 @@ ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 pulldown-cmark = { version = "0.9.2", default-features = false }
+rand.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-rand.workspace = true
 
 tree-sitter-rust = { workspace = true, optional = true }
 tree-sitter-html = { workspace = true, optional = true }

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

@@ -397,7 +397,7 @@ impl InlayMap {
         buffer_snapshot: MultiBufferSnapshot,
         mut buffer_edits: Vec<text::Edit<usize>>,
     ) -> (InlaySnapshot, Vec<InlayEdit>) {
-        let mut snapshot = &mut self.snapshot;
+        let snapshot = &mut self.snapshot;
 
         if buffer_edits.is_empty() {
             if snapshot.buffer.trailing_excerpt_update_count()
@@ -572,7 +572,6 @@ impl InlayMap {
             })
             .collect();
         let buffer_snapshot = snapshot.buffer.clone();
-        drop(snapshot);
         let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
         (snapshot, edits)
     }
@@ -635,7 +634,6 @@ impl InlayMap {
         }
         log::info!("removing inlays: {:?}", to_remove);
 
-        drop(snapshot);
         let (snapshot, edits) = self.splice(to_remove, to_insert);
         (snapshot, edits)
     }

crates/editor/src/editor.rs 🔗

@@ -28,6 +28,7 @@ use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use convert_case::{Case, Casing};
 use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
@@ -89,7 +90,7 @@ use std::{
     cmp::{self, Ordering, Reverse},
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Deref, DerefMut, Range},
+    ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
     path::Path,
     sync::Arc,
     time::{Duration, Instant},
@@ -231,6 +232,13 @@ actions!(
         SortLinesCaseInsensitive,
         ReverseLines,
         ShuffleLines,
+        ConvertToUpperCase,
+        ConvertToLowerCase,
+        ConvertToTitleCase,
+        ConvertToSnakeCase,
+        ConvertToKebabCase,
+        ConvertToUpperCamelCase,
+        ConvertToLowerCamelCase,
         Transpose,
         Cut,
         Copy,
@@ -353,6 +361,13 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::sort_lines_case_insensitive);
     cx.add_action(Editor::reverse_lines);
     cx.add_action(Editor::shuffle_lines);
+    cx.add_action(Editor::convert_to_upper_case);
+    cx.add_action(Editor::convert_to_lower_case);
+    cx.add_action(Editor::convert_to_title_case);
+    cx.add_action(Editor::convert_to_snake_case);
+    cx.add_action(Editor::convert_to_kebab_case);
+    cx.add_action(Editor::convert_to_upper_camel_case);
+    cx.add_action(Editor::convert_to_lower_camel_case);
     cx.add_action(Editor::delete_to_previous_word_start);
     cx.add_action(Editor::delete_to_previous_subword_start);
     cx.add_action(Editor::delete_to_next_word_end);
@@ -543,6 +558,7 @@ pub struct Editor {
     show_local_selections: bool,
     mode: EditorMode,
     show_gutter: bool,
+    show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
     #[allow(clippy::type_complexity)]
@@ -1375,6 +1391,7 @@ impl Editor {
             show_local_selections: true,
             mode,
             show_gutter: mode == EditorMode::Full,
+            show_wrap_guides: None,
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
@@ -1537,7 +1554,7 @@ impl Editor {
         self.collapse_matches = collapse_matches;
     }
 
-    fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
+    pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
         if self.collapse_matches {
             return range.start..range.start;
         }
@@ -4219,7 +4236,7 @@ impl Editor {
         _: &SortLinesCaseSensitive,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_lines(cx, |text| text.sort())
+        self.manipulate_lines(cx, |lines| lines.sort())
     }
 
     pub fn sort_lines_case_insensitive(
@@ -4227,7 +4244,7 @@ impl Editor {
         _: &SortLinesCaseInsensitive,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase()))
+        self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
     }
 
     pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
@@ -4265,19 +4282,19 @@ impl Editor {
             let text = buffer
                 .text_for_range(start_point..end_point)
                 .collect::<String>();
-            let mut text = text.split("\n").collect_vec();
+            let mut lines = text.split("\n").collect_vec();
 
-            let text_len = text.len();
-            callback(&mut text);
+            let lines_len = lines.len();
+            callback(&mut lines);
 
             // This is a current limitation with selections.
             // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
             debug_assert!(
-                text.len() == text_len,
+                lines.len() == lines_len,
                 "callback should not change the number of lines"
             );
 
-            edits.push((start_point..end_point, text.join("\n")));
+            edits.push((start_point..end_point, lines.join("\n")));
             let start_anchor = buffer.anchor_after(start_point);
             let end_anchor = buffer.anchor_before(end_point);
 
@@ -4304,6 +4321,97 @@ impl Editor {
         });
     }
 
+    pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_uppercase())
+    }
+
+    pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_lowercase())
+    }
+
+    pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Title))
+    }
+
+    pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Snake))
+    }
+
+    pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
+    }
+
+    pub fn convert_to_upper_camel_case(
+        &mut self,
+        _: &ConvertToUpperCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
+    }
+
+    pub fn convert_to_lower_camel_case(
+        &mut self,
+        _: &ConvertToLowerCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Camel))
+    }
+
+    fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(&str) -> String,
+    {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut new_selections = Vec::new();
+        let mut edits = Vec::new();
+        let mut selection_adjustment = 0i32;
+
+        for selection in self.selections.all::<usize>(cx) {
+            let selection_is_empty = selection.is_empty();
+
+            let (start, end) = if selection_is_empty {
+                let word_range = movement::surrounding_word(
+                    &display_map,
+                    selection.start.to_display_point(&display_map),
+                );
+                let start = word_range.start.to_offset(&display_map, Bias::Left);
+                let end = word_range.end.to_offset(&display_map, Bias::Left);
+                (start, end)
+            } else {
+                (selection.start, selection.end)
+            };
+
+            let text = buffer.text_for_range(start..end).collect::<String>();
+            let old_length = text.len() as i32;
+            let text = callback(&text);
+
+            new_selections.push(Selection {
+                start: (start as i32 - selection_adjustment) as usize,
+                end: ((start + text.len()) as i32 - selection_adjustment) as usize,
+                goal: SelectionGoal::None,
+                ..selection
+            });
+
+            selection_adjustment += old_length - text.len() as i32;
+
+            edits.push((start..end, text));
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
     pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
@@ -6374,8 +6482,8 @@ impl Editor {
                 .range
                 .to_offset(definition.target.buffer.read(cx));
 
+            let range = self.range_for_match(&range);
             if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
-                let range = self.range_for_match(&range);
                 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.select_ranges([range]);
                 });
@@ -6392,7 +6500,6 @@ impl Editor {
                         // When selecting a definition in a different buffer, disable the nav history
                         // to avoid creating a history entry at the previous cursor location.
                         pane.update(cx, |pane, _| pane.disable_history());
-                        let range = target_editor.range_for_match(&range);
                         target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                             s.select_ranges([range]);
                         });
@@ -7188,6 +7295,10 @@ impl Editor {
     pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
         let mut wrap_guides = smallvec::smallvec![];
 
+        if self.show_wrap_guides == Some(false) {
+            return wrap_guides;
+        }
+
         let settings = self.buffer.read(cx).settings_at(0, cx);
         if settings.show_wrap_guides {
             if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
@@ -7245,6 +7356,11 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+        self.show_wrap_guides = Some(show_gutter);
+        cx.notify();
+    }
+
     pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
         if let Some(buffer) = self.buffer().read(cx).as_singleton() {
             if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@@ -7433,6 +7549,78 @@ impl Editor {
         results
     }
 
+    pub fn background_highlight_row_ranges<T: 'static>(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        count: usize,
+    ) -> Vec<RangeInclusive<DisplayPoint>> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        let Some((_, ranges)) = self.background_highlights
+            .get(&TypeId::of::<T>()) else {
+                return vec![];
+            };
+
+        let start_ix = match ranges.binary_search_by(|probe| {
+            let cmp = probe.end.cmp(&search_range.start, buffer);
+            if cmp.is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Less
+            }
+        }) {
+            Ok(i) | Err(i) => i,
+        };
+        let mut push_region = |start: Option<Point>, end: Option<Point>| {
+            if let (Some(start_display), Some(end_display)) = (start, end) {
+                results.push(
+                    start_display.to_display_point(display_snapshot)
+                        ..=end_display.to_display_point(display_snapshot),
+                );
+            }
+        };
+        let mut start_row: Option<Point> = None;
+        let mut end_row: Option<Point> = None;
+        if ranges.len() > count {
+            return vec![];
+        }
+        for range in &ranges[start_ix..] {
+            if range.start.cmp(&search_range.end, buffer).is_ge() {
+                break;
+            }
+            let end = range.end.to_point(buffer);
+            if let Some(current_row) = &end_row {
+                if end.row == current_row.row {
+                    continue;
+                }
+            }
+            let start = range.start.to_point(buffer);
+
+            if start_row.is_none() {
+                assert_eq!(end_row, None);
+                start_row = Some(start);
+                end_row = Some(end);
+                continue;
+            }
+            if let Some(current_end) = end_row.as_mut() {
+                if start.row > current_end.row + 1 {
+                    push_region(start_row, end_row);
+                    start_row = Some(start);
+                    end_row = Some(end);
+                } else {
+                    // Merge two hunks.
+                    *current_end = end;
+                }
+            } else {
+                unreachable!();
+            }
+        }
+        // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+        push_region(start_row, end_row);
+        results
+    }
+
     pub fn highlight_text<T: 'static>(
         &mut self,
         ranges: Vec<Range<Anchor>>,

crates/editor/src/editor_tests.rs 🔗

@@ -48,36 +48,40 @@ fn test_edit_events(cx: &mut TestAppContext) {
     });
 
     let events = Rc::new(RefCell::new(Vec::new()));
-    let (_, editor1) = cx.add_window({
-        let events = events.clone();
-        |cx| {
-            cx.subscribe(&cx.handle(), move |_, _, event, _| {
-                if matches!(
-                    event,
-                    Event::Edited | Event::BufferEdited | Event::DirtyChanged
-                ) {
-                    events.borrow_mut().push(("editor1", event.clone()));
-                }
-            })
-            .detach();
-            Editor::for_buffer(buffer.clone(), None, cx)
-        }
-    });
-    let (_, editor2) = cx.add_window({
-        let events = events.clone();
-        |cx| {
-            cx.subscribe(&cx.handle(), move |_, _, event, _| {
-                if matches!(
-                    event,
-                    Event::Edited | Event::BufferEdited | Event::DirtyChanged
-                ) {
-                    events.borrow_mut().push(("editor2", event.clone()));
-                }
-            })
-            .detach();
-            Editor::for_buffer(buffer.clone(), None, cx)
-        }
-    });
+    let editor1 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor1", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
+    let editor2 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor2", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
     assert_eq!(mem::take(&mut *events.borrow_mut()), []);
 
     // Mutating editor 1 will emit an `Edited` event only for that editor.
@@ -173,7 +177,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
     let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
     let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let editor = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         editor.start_transaction_at(now, cx);
@@ -343,10 +349,12 @@ fn test_ime_composition(cx: &mut TestAppContext) {
 fn test_selection_with_mouse(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     editor.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
     });
@@ -410,10 +418,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
 fn test_canceling_pending_selection(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
@@ -456,10 +466,12 @@ fn test_clone(cx: &mut TestAppContext) {
         true,
     );
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&text, cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&text, cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
@@ -473,9 +485,11 @@ fn test_clone(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, cloned_editor) = editor.update(cx, |editor, cx| {
-        cx.add_window(Default::default(), |cx| editor.clone(cx))
-    });
+    let cloned_editor = editor
+        .update(cx, |editor, cx| {
+            cx.add_window(Default::default(), |cx| editor.clone(cx))
+        })
+        .root(cx);
 
     let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
     let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
@@ -509,9 +523,10 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, [], cx).await;
-    let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let workspace = window.root(cx);
     let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-    cx.add_view(window_id, |cx| {
+    window.add_view(cx, |cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
         let mut editor = build_editor(buffer.clone(), cx);
         let handle = cx.handle();
@@ -618,10 +633,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 fn test_cancel(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
@@ -661,9 +678,10 @@ fn test_cancel(cx: &mut TestAppContext) {
 fn test_fold_action(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(
-            &"
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                &"
                 impl Foo {
                     // Hello!
 
@@ -680,11 +698,12 @@ fn test_fold_action(cx: &mut TestAppContext) {
                     }
                 }
             "
-            .unindent(),
-            cx,
-        );
-        build_editor(buffer.clone(), cx)
-    });
+                .unindent(),
+                cx,
+            );
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -752,7 +771,9 @@ fn test_move_cursor(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let view = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
 
     buffer.update(cx, |buffer, cx| {
         buffer.edit(
@@ -827,10 +848,12 @@ fn test_move_cursor(cx: &mut TestAppContext) {
 fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     assert_eq!('ⓐ'.len_utf8(), 3);
     assert_eq!('α'.len_utf8(), 2);
@@ -932,10 +955,12 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
@@ -982,10 +1007,12 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
 fn test_beginning_end_of_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\n  def", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\n  def", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -1145,10 +1172,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
 fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -1197,10 +1226,13 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
 fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer =
+                MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.set_wrap_width(Some(140.), cx);
@@ -1257,7 +1289,8 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
     let mut cx = EditorTestContext::new(cx).await;
 
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
 
     cx.set_state(
         &r#"ˇone
@@ -1368,7 +1401,8 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
     let mut cx = EditorTestContext::new(cx).await;
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5));
+    let window = cx.window;
+    window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
 
     cx.set_state(
         &r#"ˇone
@@ -1406,7 +1440,8 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorTestContext::new(cx).await;
 
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
 
     cx.set_state(
         &r#"
@@ -1530,10 +1565,12 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
 fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("one two three four", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("one two three four", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -1566,10 +1603,12 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
 fn test_newline(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -1589,9 +1628,10 @@ fn test_newline(cx: &mut TestAppContext) {
 fn test_newline_with_old_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(
-            "
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                "
                 a
                 b(
                     X
@@ -1600,19 +1640,20 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
                     X
                 )
             "
-            .unindent()
-            .as_str(),
-            cx,
-        );
-        let mut editor = build_editor(buffer.clone(), cx);
-        editor.change_selections(None, cx, |s| {
-            s.select_ranges([
-                Point::new(2, 4)..Point::new(2, 5),
-                Point::new(5, 4)..Point::new(5, 5),
-            ])
-        });
-        editor
-    });
+                .unindent()
+                .as_str(),
+                cx,
+            );
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(2, 4)..Point::new(2, 5),
+                    Point::new(5, 4)..Point::new(5, 5),
+                ])
+            });
+            editor
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         // Edit the buffer directly, deleting ranges surrounding the editor's selections
@@ -1817,12 +1858,14 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
 fn test_insert_with_old_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
-        let mut editor = build_editor(buffer.clone(), cx);
-        editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
+            editor
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         // Edit the buffer directly, deleting ranges surrounding the editor's selections
@@ -2329,10 +2372,12 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
 fn test_delete_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2352,10 +2397,12 @@ fn test_delete_line(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
@@ -2650,14 +2697,94 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_manipulate_text(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test convert_to_upper_case()
+    cx.set_state(indoc! {"
+        «hello worldˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «HELLO WORLDˇ»
+    "});
+
+    // Test convert_to_lower_case()
+    cx.set_state(indoc! {"
+        «HELLO WORLDˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «hello worldˇ»
+    "});
+
+    // From here on out, test more complex cases of manipulate_text()
+
+    // Test no selection case - should affect words cursors are in
+    // Cursor at beginning, middle, and end of word
+    cx.set_state(indoc! {"
+        ˇhello big beauˇtiful worldˇ
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
+    "});
+
+    // Test multiple selections on a single line and across multiple lines
+    cx.set_state(indoc! {"
+        «Theˇ» quick «brown
+        foxˇ» jumps «overˇ»
+        the «lazyˇ» dog
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «THEˇ» quick «BROWN
+        FOXˇ» jumps «OVERˇ»
+        the «LAZYˇ» dog
+    "});
+
+    // Test case where text length grows
+    cx.set_state(indoc! {"
+        «tschüߡ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «TSCHÜSSˇ»
+    "});
+
+    // Test to make sure we don't crash when text shrinks
+    cx.set_state(indoc! {"
+        aaa_bbbˇ
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaBbbˇ»
+    "});
+
+    // Test to make sure we all aware of the fact that each word can grow and shrink
+    // Final selections should be aware of this fact
+    cx.set_state(indoc! {"
+        aaa_bˇbb bbˇb_ccc ˇccc_ddd
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
+    "});
+}
+
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2680,10 +2807,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2707,10 +2836,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
 fn test_move_line_up_down(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.fold_ranges(
             vec![
@@ -2806,10 +2937,12 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
 fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     editor.update(cx, |editor, cx| {
         let snapshot = editor.buffer.read(cx).snapshot(cx);
         editor.insert_blocks(
@@ -2834,102 +2967,94 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
 fn test_transpose(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bac");
-            assert_eq!(editor.selections.ranges(cx), [2..2]);
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [2..2]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bca");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bca");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bac");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acb\nde");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acb\nde");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbd\ne");
-            assert_eq!(editor.selections.ranges(cx), [5..5]);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [5..5]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbde\n");
-            assert_eq!(editor.selections.ranges(cx), [6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbde\n");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbd\ne");
-            assert_eq!(editor.selections.ranges(cx), [6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bacd\ne");
-            assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bacd\ne");
+        assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcade\n");
-            assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcda\ne");
-            assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcda\ne");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcade\n");
-            assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcaed\n");
-            assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcaed\n");
+        assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀🍐✋");
-            assert_eq!(editor.selections.ranges(cx), [8..8]);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀🍐✋");
+        assert_eq!(editor.selections.ranges(cx), [8..8]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀✋🍐");
-            assert_eq!(editor.selections.ranges(cx), [11..11]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀✋🍐");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀🍐✋");
-            assert_eq!(editor.selections.ranges(cx), [11..11]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀🍐✋");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 }
 
 #[gpui::test]
@@ -3132,10 +3257,12 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
 fn test_select_all(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.select_all(&SelectAll, cx);
         assert_eq!(
@@ -3149,10 +3276,12 @@ fn test_select_all(cx: &mut TestAppContext) {
 fn test_select_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -3196,10 +3325,12 @@ fn test_select_line(cx: &mut TestAppContext) {
 fn test_split_selection_into_lines(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.fold_ranges(
             vec![
@@ -3267,10 +3398,12 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
 fn test_add_selection_above_below(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -3555,7 +3688,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -3718,7 +3851,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
         .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4281,7 +4414,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -4429,7 +4562,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
         .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4519,7 +4652,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
     );
 
     let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
 
     editor.update(cx, |editor, cx| {
         let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
@@ -4649,7 +4782,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -4761,7 +4894,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -4875,7 +5008,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
 
     let format = editor.update(cx, |editor, cx| {
@@ -5653,7 +5786,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx));
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
     view.update(cx, |view, cx| {
         assert_eq!(view.text(cx), "aaaa\nbbbb");
         view.change_selections(None, cx, |s| {
@@ -5723,7 +5856,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx));
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
     view.update(cx, |view, cx| {
         let (expected_text, selection_ranges) = marked_text_ranges(
             indoc! {"
@@ -5799,22 +5932,24 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, editor) = cx.add_window(|cx| {
-        let mut editor = build_editor(multibuffer.clone(), cx);
-        let snapshot = editor.snapshot(cx);
-        editor.change_selections(None, cx, |s| {
-            s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
-        });
-        editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
-        assert_eq!(
-            editor.selections.ranges(cx),
-            [
-                Point::new(1, 3)..Point::new(1, 3),
-                Point::new(2, 1)..Point::new(2, 1),
-            ]
-        );
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
+            });
+            editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [
+                    Point::new(1, 3)..Point::new(1, 3),
+                    Point::new(2, 1)..Point::new(2, 1),
+                ]
+            );
+            editor
+        })
+        .root(cx);
 
     // Refreshing selections is a no-op when excerpts haven't changed.
     editor.update(cx, |editor, cx| {
@@ -5884,16 +6019,18 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, editor) = cx.add_window(|cx| {
-        let mut editor = build_editor(multibuffer.clone(), cx);
-        let snapshot = editor.snapshot(cx);
-        editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
-        assert_eq!(
-            editor.selections.ranges(cx),
-            [Point::new(1, 3)..Point::new(1, 3)]
-        );
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [Point::new(1, 3)..Point::new(1, 3)]
+            );
+            editor
+        })
+        .root(cx);
 
     multibuffer.update(cx, |multibuffer, cx| {
         multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);

crates/editor/src/element.rs 🔗

@@ -172,6 +172,10 @@ impl EditorElement {
             .on_drag(MouseButton::Left, {
                 let position_map = position_map.clone();
                 move |event, editor, cx| {
+                    if event.end {
+                        return;
+                    }
+
                     if !Self::mouse_dragged(
                         editor,
                         event.platform_event,
@@ -542,8 +546,20 @@ impl EditorElement {
                 });
             }
 
+            let scroll_left =
+                layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width;
+
             for (wrap_position, active) in layout.wrap_guides.iter() {
-                let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.;
+                let x =
+                    (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.)
+                        - scroll_left;
+
+                if x < text_bounds.origin_x()
+                    || (layout.show_scrollbars && x > self.scrollbar_left(&bounds))
+                {
+                    continue;
+                }
+
                 let color = if *active {
                     self.style.active_wrap_guide
                 } else {
@@ -1032,6 +1048,10 @@ impl EditorElement {
         scene.pop_layer();
     }
 
+    fn scrollbar_left(&self, bounds: &RectF) -> f32 {
+        bounds.max_x() - self.style.theme.scrollbar.width
+    }
+
     fn paint_scrollbar(
         &mut self,
         scene: &mut SceneBuilder,
@@ -1050,7 +1070,7 @@ impl EditorElement {
         let top = bounds.min_y();
         let bottom = bounds.max_y();
         let right = bounds.max_x();
-        let left = right - style.width;
+        let left = self.scrollbar_left(&bounds);
         let row_range = &layout.scrollbar_row_range;
         let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
 
@@ -1087,8 +1107,6 @@ impl EditorElement {
             if layout.is_singleton && scrollbar_settings.selections {
                 let start_anchor = Anchor::min();
                 let end_anchor = Anchor::max();
-                let mut start_row = None;
-                let mut end_row = None;
                 let color = scrollbar_theme.selections;
                 let border = Border {
                     width: 1.,
@@ -1099,54 +1117,32 @@ impl EditorElement {
                     bottom: false,
                     left: true,
                 };
-                let mut push_region = |start, end| {
-                    if let (Some(start_display), Some(end_display)) = (start, end) {
-                        let start_y = y_for_row(start_display as f32);
-                        let mut end_y = y_for_row(end_display as f32);
-                        if end_y - start_y < 1. {
-                            end_y = start_y + 1.;
-                        }
-                        let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
-
-                        scene.push_quad(Quad {
-                            bounds,
-                            background: Some(color),
-                            border,
-                            corner_radius: style.thumb.corner_radius,
-                        })
+                let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
+                    let start_y = y_for_row(start.row() as f32);
+                    let mut end_y = y_for_row(end.row() as f32);
+                    if end_y - start_y < 1. {
+                        end_y = start_y + 1.;
                     }
+                    let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+                    scene.push_quad(Quad {
+                        bounds,
+                        background: Some(color),
+                        border,
+                        corner_radius: style.thumb.corner_radius,
+                    })
                 };
-                for (row, _) in &editor
-                    .background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
+                let background_ranges = editor
+                    .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
                         start_anchor..end_anchor,
                         &layout.position_map.snapshot,
-                        &theme,
-                    )
-                {
-                    let start_display = row.start;
-                    let end_display = row.end;
-
-                    if start_row.is_none() {
-                        assert_eq!(end_row, None);
-                        start_row = Some(start_display.row());
-                        end_row = Some(end_display.row());
-                        continue;
-                    }
-                    if let Some(current_end) = end_row.as_mut() {
-                        if start_display.row() > *current_end + 1 {
-                            push_region(start_row, end_row);
-                            start_row = Some(start_display.row());
-                            end_row = Some(end_display.row());
-                        } else {
-                            // Merge two hunks.
-                            *current_end = end_display.row();
-                        }
-                    } else {
-                        unreachable!();
-                    }
+                        50000,
+                    );
+                for row in background_ranges {
+                    let start = row.start();
+                    let end = row.end();
+                    push_region(*start, *end);
                 }
-                // We might still have a hunk that was not rendered (if there was a search hit on the last line)
-                push_region(start_row, end_row);
             }
 
             if layout.is_singleton && scrollbar_settings.git_diff {
@@ -1235,6 +1231,10 @@ impl EditorElement {
                 })
                 .on_drag(MouseButton::Left, {
                     move |event, editor: &mut Editor, cx| {
+                        if event.end {
+                            return;
+                        }
+
                         let y = event.prev_mouse_position.y();
                         let new_y = event.position.y();
                         if thumb_top < y && y < thumb_bottom {
@@ -2978,10 +2978,12 @@ mod tests {
     fn test_layout_line_numbers(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
 
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
         let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
 
         let layouts = editor.update(cx, |editor, cx| {
@@ -2997,10 +2999,12 @@ mod tests {
     fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
 
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple("", cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple("", cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
 
         editor.update(cx, |editor, cx| {
             editor.set_placeholder_text("hello", cx);
@@ -3214,10 +3218,12 @@ mod tests {
         info!(
             "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
         );
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple(&input_text, cx);
-            Editor::new(editor_mode, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&input_text, cx);
+                Editor::new(editor_mode, buffer, None, None, cx)
+            })
+            .root(cx);
 
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, layout_state) = editor.update(cx, |editor, cx| {

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -571,7 +571,6 @@ fn new_update_task(
                     if let Some(buffer) =
                         refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
                     {
-                        drop(refresh_multi_buffer);
                         editor.inlay_hint_cache.update_tasks.insert(
                             pending_refresh_query.excerpt_id,
                             UpdateTask {
@@ -1136,7 +1135,9 @@ mod tests {
                 )
                 .await;
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1836,7 +1837,9 @@ mod tests {
         .await;
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1989,7 +1992,9 @@ mod tests {
         project.update(cx, |project, _| {
             project.languages().add(Arc::clone(&language))
         });
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -2075,8 +2080,9 @@ mod tests {
 
         deterministic.run_until_parked();
         cx.foreground().run_until_parked();
-        let (_, editor) =
-            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
         let editor_edited = Arc::new(AtomicBool::new(false));
         let fake_server = fake_servers.next().await.unwrap();
         let closure_editor_edited = Arc::clone(&editor_edited);
@@ -2328,7 +2334,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         project.update(cx, |project, _| {
             project.languages().add(Arc::clone(&language))
         });
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -2373,8 +2381,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
 
         deterministic.run_until_parked();
         cx.foreground().run_until_parked();
-        let (_, editor) =
-            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
         let editor_edited = Arc::new(AtomicBool::new(false));
         let fake_server = fake_servers.next().await.unwrap();
         let closure_editor_edited = Arc::clone(&editor_edited);
@@ -2562,7 +2571,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
 
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()

crates/editor/src/items.rs 🔗

@@ -28,7 +28,10 @@ use std::{
     path::{Path, PathBuf},
 };
 use text::Selection;
-use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
+use util::{
+    paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
+    ResultExt, TryFutureExt,
+};
 use workspace::item::{BreadcrumbText, FollowableItemHandle};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@@ -546,9 +549,7 @@ impl Item for Editor {
             .and_then(|f| f.as_local())?
             .abs_path(cx);
 
-        let file_path = util::paths::compact(&file_path)
-            .to_string_lossy()
-            .to_string();
+        let file_path = file_path.compact().to_string_lossy().to_string();
 
         Some(file_path.into())
     }

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

@@ -69,7 +69,8 @@ impl<'a> EditorLspTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
@@ -98,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> {
         Self {
             cx: EditorTestContext {
                 cx,
-                window_id,
+                window: window.into(),
                 editor,
             },
             lsp,

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

@@ -3,7 +3,8 @@ use crate::{
 };
 use futures::Future;
 use gpui::{
-    keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
+    keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
+    ViewContext, ViewHandle,
 };
 use indoc::indoc;
 use language::{Buffer, BufferSnapshot};
@@ -21,7 +22,7 @@ use super::build_editor;
 
 pub struct EditorTestContext<'a> {
     pub cx: &'a mut gpui::TestAppContext,
-    pub window_id: usize,
+    pub window: AnyWindowHandle,
     pub editor: ViewHandle<Editor>,
 }
 
@@ -32,16 +33,14 @@ impl<'a> EditorTestContext<'a> {
         let buffer = project
             .update(cx, |project, cx| project.create_buffer("", None, cx))
             .unwrap();
-        let (window_id, editor) = cx.update(|cx| {
-            cx.add_window(Default::default(), |cx| {
-                cx.focus_self();
-                build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
-            })
+        let window = cx.add_window(|cx| {
+            cx.focus_self();
+            build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
         });
-
+        let editor = window.root(cx);
         Self {
             cx,
-            window_id,
+            window: window.into(),
             editor,
         }
     }
@@ -113,7 +112,8 @@ impl<'a> EditorTestContext<'a> {
         let keystroke_under_test_handle =
             self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
-        self.cx.dispatch_keystroke(self.window_id, keystroke, false);
+
+        self.cx.dispatch_keystroke(self.window, keystroke, false);
         keystroke_under_test_handle
     }
 

crates/file_finder/src/file_finder.rs 🔗

@@ -617,8 +617,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         finder
@@ -631,8 +632,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -671,8 +672,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 
         let file_query = &first_file_name[..3];
@@ -704,8 +706,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -754,8 +756,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 
         let file_query = &first_file_name[..3];
@@ -787,8 +790,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -837,19 +840,23 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         let query = test_path_like("hi");
         finder
@@ -931,19 +938,23 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
         finder
             .update(cx, |f, cx| {
                 f.delegate_mut().spawn_search(test_path_like("hi"), cx)
@@ -967,19 +978,23 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -1015,61 +1030,6 @@ mod tests {
         finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
     }
 
-    #[gpui::test]
-    async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
-        let app_state = init_test(cx);
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                "/root",
-                json!({
-                    "dir1": { "a.txt": "" },
-                    "dir2": { "a.txt": "" }
-                }),
-            )
-            .await;
-
-        let project = Project::test(
-            app_state.fs.clone(),
-            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
-            cx,
-        )
-        .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
-                    cx,
-                ),
-                cx,
-            )
-        });
-
-        // Run a search that matches two files with the same relative path.
-        finder
-            .update(cx, |f, cx| {
-                f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
-            })
-            .await;
-
-        // Can switch between different matches with the same relative path.
-        finder.update(cx, |finder, cx| {
-            let delegate = finder.delegate_mut();
-            assert_eq!(delegate.matches.len(), 2);
-            assert_eq!(delegate.selected_index(), 0);
-            delegate.set_selected_index(1, cx);
-            assert_eq!(delegate.selected_index(), 1);
-            delegate.set_selected_index(0, cx);
-            assert_eq!(delegate.selected_index(), 0);
-        });
-    }
-
     #[gpui::test]
     async fn test_path_distance_ordering(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
@@ -1089,7 +1049,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
@@ -1103,18 +1065,20 @@ mod tests {
             worktree_id,
             path: Arc::from(Path::new("/root/dir2/b.txt")),
         }));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    b_path,
-                    Vec::new(),
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        b_path,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         finder
             .update(cx, |f, cx| {
@@ -1151,19 +1115,23 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
         finder
             .update(cx, |f, cx| {
                 f.delegate_mut().spawn_search(test_path_like("dir"), cx)
@@ -1198,7 +1166,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        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);
@@ -1216,7 +1185,7 @@ mod tests {
             "fir",
             1,
             "first.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1231,7 +1200,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1253,7 +1222,7 @@ mod tests {
             "thi",
             1,
             "third.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1285,7 +1254,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1324,7 +1293,7 @@ mod tests {
             "thi",
             1,
             "third.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1404,7 +1373,8 @@ mod tests {
         .detach();
         deterministic.run_until_parked();
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        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,);
@@ -1439,7 +1409,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1461,7 +1431,7 @@ mod tests {
             "fir",
             1,
             "first.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1493,12 +1463,12 @@ mod tests {
         input: &str,
         expected_matches: usize,
         expected_editor_title: &str,
-        window_id: usize,
+        window: gpui::AnyWindowHandle,
         workspace: &ViewHandle<Workspace>,
         deterministic: &gpui::executor::Deterministic,
         cx: &mut gpui::TestAppContext,
     ) -> Vec<FoundPath> {
-        cx.dispatch_action(window_id, Toggle);
+        cx.dispatch_action(window, Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         finder
             .update(cx, |finder, cx| {
@@ -1515,8 +1485,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window, SelectNext);
+        cx.dispatch_action(window, Confirm);
         deterministic.run_until_parked();
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())

crates/go_to_line/src/go_to_line.rs 🔗

@@ -135,7 +135,7 @@ impl Entity for GoToLine {
 
     fn release(&mut self, cx: &mut AppContext) {
         let scroll_position = self.prev_scroll_position.take();
-        cx.update_window(self.active_editor.window_id(), |cx| {
+        self.active_editor.window().update(cx, |cx| {
             self.active_editor.update(cx, |editor, cx| {
                 editor.highlight_rows(None);
                 if let Some(scroll_position) = scroll_position {

crates/gpui/Cargo.toml 🔗

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

crates/gpui/src/app.rs 🔗

@@ -23,6 +23,8 @@ use std::{
 };
 
 use anyhow::{anyhow, Context, Result};
+
+use derive_more::Deref;
 use parking_lot::Mutex;
 use postage::oneshot;
 use smallvec::SmallVec;
@@ -131,8 +133,20 @@ pub trait BorrowAppContext {
 }
 
 pub trait BorrowWindowContext {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T;
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T;
+    type Result<T>;
+
+    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T;
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>;
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T;
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>;
 }
 
 #[derive(Clone)]
@@ -295,13 +309,12 @@ impl App {
         result
     }
 
-    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, callback: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
         let mut state = self.0.borrow_mut();
-        let result = state.update_window(window_id, callback);
+        let result = state.update_window(window, callback);
         state.pending_notifications.clear();
         result
     }
@@ -328,67 +341,8 @@ impl AsyncAppContext {
         self.0.borrow_mut().update(callback)
     }
 
-    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
-        &self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.0.borrow_mut().read_window(window_id, callback)
-    }
-
-    pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.0.borrow_mut().update_window(window_id, callback)
-    }
-
-    pub fn debug_elements(&self, window_id: usize) -> Option<json::Value> {
-        self.0.borrow().read_window(window_id, |cx| {
-            let root_view = cx.window.root_view();
-            let root_element = cx.window.rendered_views.get(&root_view.id())?;
-            root_element.debug(cx).log_err()
-        })?
-    }
-
-    pub fn dispatch_action(
-        &mut self,
-        window_id: usize,
-        view_id: usize,
-        action: &dyn Action,
-    ) -> Result<()> {
-        self.0
-            .borrow_mut()
-            .update_window(window_id, |window| {
-                window.dispatch_action(Some(view_id), action);
-            })
-            .ok_or_else(|| anyhow!("window not found"))
-    }
-
-    pub fn available_actions(
-        &self,
-        window_id: usize,
-        view_id: usize,
-    ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        self.read_window(window_id, |cx| cx.available_actions(view_id))
-            .unwrap_or_default()
-    }
-
-    pub fn has_window(&self, window_id: usize) -> bool {
-        self.read(|cx| cx.windows.contains_key(&window_id))
-    }
-
-    pub fn window_is_active(&self, window_id: usize) -> bool {
-        self.read(|cx| cx.windows.get(&window_id).map_or(false, |w| w.is_active))
-    }
-
-    pub fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
-        self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone()))
-    }
-
-    pub fn window_ids(&self) -> Vec<usize> {
-        self.read(|cx| cx.windows.keys().copied().collect())
+    pub fn windows(&self) -> Vec<AnyWindowHandle> {
+        self.0.borrow().windows().collect()
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -403,7 +357,7 @@ impl AsyncAppContext {
         &mut self,
         window_options: WindowOptions,
         build_root_view: F,
-    ) -> (usize, ViewHandle<T>)
+    ) -> WindowHandle<T>
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
@@ -411,25 +365,6 @@ impl AsyncAppContext {
         self.update(|cx| cx.add_window(window_options, build_root_view))
     }
 
-    pub fn remove_window(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| cx.remove_window());
-    }
-
-    pub fn activate_window(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| cx.activate_window());
-    }
-
-    // TODO: Can we eliminate this method and move it to WindowContext then call it with update_window?s
-    pub fn prompt(
-        &mut self,
-        window_id: usize,
-        level: PromptLevel,
-        msg: &str,
-        answers: &[&str],
-    ) -> Option<oneshot::Receiver<usize>> {
-        self.update_window(window_id, |cx| cx.prompt(level, msg, answers))
-    }
-
     pub fn platform(&self) -> Arc<dyn Platform> {
         self.0.borrow().platform().clone()
     }
@@ -453,6 +388,42 @@ impl BorrowAppContext for AsyncAppContext {
     }
 }
 
+impl BorrowWindowContext for AsyncAppContext {
+    type Result<T> = Option<T>;
+
+    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T,
+    {
+        self.0.borrow().read_with(|cx| cx.read_window(window, f))
+    }
+
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        self.0
+            .borrow_mut()
+            .update(|cx| cx.read_window_optional(window, f))
+    }
+
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
+        self.0.borrow_mut().update(|cx| cx.update_window(window, f))
+    }
+
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        self.0
+            .borrow_mut()
+            .update(|cx| cx.update_window_optional(window, f))
+    }
+}
+
 type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut WindowContext, usize);
 type GlobalActionCallback = dyn FnMut(&dyn Action, &mut AppContext);
 
@@ -474,9 +445,9 @@ type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> b
 
 pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
-    views: HashMap<(usize, usize), Box<dyn AnyView>>,
-    views_metadata: HashMap<(usize, usize), ViewMetadata>,
-    windows: HashMap<usize, Window>,
+    views: HashMap<(AnyWindowHandle, usize), Box<dyn AnyView>>,
+    views_metadata: HashMap<(AnyWindowHandle, usize), ViewMetadata>,
+    windows: HashMap<AnyWindowHandle, Window>,
     globals: HashMap<TypeId, Box<dyn Any>>,
     element_states: HashMap<ElementStateId, Box<dyn Any>>,
     background: Arc<executor::Background>,
@@ -495,8 +466,8 @@ pub struct AppContext {
     // Action Types -> Action Handlers
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
     keystroke_matcher: KeymapMatcher,
-    next_entity_id: usize,
-    next_window_id: usize,
+    next_id: usize,
+    // next_window: AnyWindowHandle,
     next_subscription_id: usize,
     frame_count: usize,
 
@@ -507,10 +478,10 @@ pub struct AppContext {
     focus_observations: CallbackCollection<usize, FocusObservationCallback>,
     release_observations: CallbackCollection<usize, ReleaseObservationCallback>,
     action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
-    window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
-    window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
-    window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
-    keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
+    window_activation_observations: CallbackCollection<AnyWindowHandle, WindowActivationCallback>,
+    window_fullscreen_observations: CallbackCollection<AnyWindowHandle, WindowFullscreenCallback>,
+    window_bounds_observations: CallbackCollection<AnyWindowHandle, WindowBoundsCallback>,
+    keystroke_observations: CallbackCollection<AnyWindowHandle, KeystrokeCallback>,
     active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>,
 
     foreground: Rc<executor::Foreground>,
@@ -555,8 +526,7 @@ impl AppContext {
             actions: Default::default(),
             global_actions: Default::default(),
             keystroke_matcher: KeymapMatcher::default(),
-            next_entity_id: 0,
-            next_window_id: 0,
+            next_id: 0,
             next_subscription_id: 0,
             frame_count: 0,
             subscriptions: Default::default(),
@@ -757,13 +727,13 @@ impl AppContext {
         }
     }
 
-    pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
-        Some(self.views.get(&(window_id, view_id))?.ui_name())
+    pub fn view_ui_name(&self, window: AnyWindowHandle, view_id: usize) -> Option<&'static str> {
+        Some(self.views.get(&(window, view_id))?.ui_name())
     }
 
-    pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
+    pub fn view_type_id(&self, window: AnyWindowHandle, view_id: usize) -> Option<TypeId> {
         self.views_metadata
-            .get(&(window_id, view_id))
+            .get(&(window, view_id))
             .map(|metadata| metadata.type_id)
     }
 
@@ -784,39 +754,22 @@ impl AppContext {
         result
     }
 
-    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(
         &self,
-        window_id: usize,
+        handle: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        let window = self.windows.get(&window_id)?;
-        let window_context = WindowContext::immutable(self, &window, window_id);
+        let window = self.windows.get(&handle)?;
+        let window_context = WindowContext::immutable(self, &window, handle);
         Some(callback(&window_context))
     }
 
-    pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.update(|app_context| {
-            let mut window = app_context.windows.remove(&window_id)?;
-            let mut window_context = WindowContext::mutable(app_context, &mut window, window_id);
-            let result = callback(&mut window_context);
-            if !window_context.removed {
-                app_context.windows.insert(window_id, window);
-            }
-            Some(result)
-        })
-    }
-
     pub fn update_active_window<T, F: FnOnce(&mut WindowContext) -> T>(
         &mut self,
         callback: F,
     ) -> Option<T> {
-        self.platform
-            .main_window_id()
-            .and_then(|id| self.update_window(id, callback))
+        self.active_window()
+            .and_then(|window| window.update(self, callback))
     }
 
     pub fn prompt_for_paths(
@@ -1054,10 +1007,10 @@ impl AppContext {
         }
     }
 
-    fn notify_view(&mut self, window_id: usize, view_id: usize) {
+    fn notify_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         if self.pending_notifications.insert(view_id) {
             self.pending_effects
-                .push_back(Effect::ViewNotification { window_id, view_id });
+                .push_back(Effect::ViewNotification { window, view_id });
         }
     }
 
@@ -1075,13 +1028,13 @@ impl AppContext {
     pub fn is_action_available(&self, action: &dyn Action) -> bool {
         let mut available_in_window = false;
         let action_id = action.id();
-        if let Some(window_id) = self.platform.main_window_id() {
+        if let Some(window) = self.active_window() {
             available_in_window = self
-                .read_window(window_id, |cx| {
+                .read_window(window, |cx| {
                     if let Some(focused_view_id) = cx.focused_view_id() {
                         for view_id in cx.ancestors(focused_view_id) {
                             if let Some(view_metadata) =
-                                cx.views_metadata.get(&(window_id, view_id))
+                                cx.views_metadata.get(&(cx.window_handle, view_id))
                             {
                                 if let Some(actions) = cx.actions.get(&view_metadata.type_id) {
                                     if actions.contains_key(&action_id) {
@@ -1129,6 +1082,12 @@ impl AppContext {
         self.keystroke_matcher.clear_bindings();
     }
 
+    pub fn binding_for_action(&self, action: &dyn Action) -> Option<&Binding> {
+        self.keystroke_matcher
+            .bindings_for_action(action.id())
+            .find(|binding| binding.action().eq(action))
+    }
+
     pub fn default_global<T: 'static + Default>(&mut self) -> &T {
         let type_id = TypeId::of::<T>();
         self.update(|this| {
@@ -1221,7 +1180,7 @@ impl AppContext {
         F: FnOnce(&mut ModelContext<T>) -> T,
     {
         self.update(|this| {
-            let model_id = post_inc(&mut this.next_entity_id);
+            let model_id = post_inc(&mut this.next_id);
             let handle = ModelHandle::new(model_id, &this.ref_counts);
             let mut cx = ModelContext::new(this, model_id);
             let model = build_model(&mut cx);
@@ -1295,46 +1254,40 @@ impl AppContext {
         &mut self,
         window_options: WindowOptions,
         build_root_view: F,
-    ) -> (usize, ViewHandle<V>)
+    ) -> WindowHandle<V>
     where
         V: View,
         F: FnOnce(&mut ViewContext<V>) -> V,
     {
         self.update(|this| {
-            let window_id = post_inc(&mut this.next_window_id);
+            let handle = WindowHandle::<V>::new(post_inc(&mut this.next_id));
             let platform_window =
                 this.platform
-                    .open_window(window_id, window_options, this.foreground.clone());
-            let window = this.build_window(window_id, platform_window, build_root_view);
-            let root_view = window.root_view().clone().downcast::<V>().unwrap();
-            this.windows.insert(window_id, window);
-            (window_id, root_view)
+                    .open_window(handle.into(), window_options, this.foreground.clone());
+            let window = this.build_window(handle.into(), platform_window, build_root_view);
+            this.windows.insert(handle.into(), window);
+            handle
         })
     }
 
-    pub fn add_status_bar_item<V, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<V>)
+    pub fn add_status_bar_item<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
     where
         V: View,
         F: FnOnce(&mut ViewContext<V>) -> V,
     {
         self.update(|this| {
-            let window_id = post_inc(&mut this.next_window_id);
-            let platform_window = this.platform.add_status_item(window_id);
-            let window = this.build_window(window_id, platform_window, build_root_view);
-            let root_view = window.root_view().clone().downcast::<V>().unwrap();
-
-            this.windows.insert(window_id, window);
-            this.update_window(window_id, |cx| {
-                root_view.update(cx, |view, cx| view.focus_in(cx.handle().into_any(), cx))
-            });
-
-            (window_id, root_view)
+            let handle = WindowHandle::<V>::new(post_inc(&mut this.next_id));
+            let platform_window = this.platform.add_status_item(handle.into());
+            let window = this.build_window(handle.into(), platform_window, build_root_view);
+            this.windows.insert(handle.into(), window);
+            handle.update_root(this, |view, cx| view.focus_in(cx.handle().into_any(), cx));
+            handle
         })
     }
 
     pub fn build_window<V, F>(
         &mut self,
-        window_id: usize,
+        handle: AnyWindowHandle,
         mut platform_window: Box<dyn platform::Window>,
         build_root_view: F,
     ) -> Window
@@ -1346,7 +1299,7 @@ impl AppContext {
             let mut app = self.upgrade();
 
             platform_window.on_event(Box::new(move |event| {
-                app.update_window(window_id, |cx| {
+                app.update_window(handle, |cx| {
                     if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
                         if cx.dispatch_keystroke(keystroke) {
                             return true;
@@ -1362,35 +1315,35 @@ impl AppContext {
         {
             let mut app = self.upgrade();
             platform_window.on_active_status_change(Box::new(move |is_active| {
-                app.update(|cx| cx.window_changed_active_status(window_id, is_active))
+                app.update(|cx| cx.window_changed_active_status(handle, is_active))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_resize(Box::new(move || {
-                app.update(|cx| cx.window_was_resized(window_id))
+                app.update(|cx| cx.window_was_resized(handle))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_moved(Box::new(move || {
-                app.update(|cx| cx.window_was_moved(window_id))
+                app.update(|cx| cx.window_was_moved(handle))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_fullscreen(Box::new(move |is_fullscreen| {
-                app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen))
+                app.update(|cx| cx.window_was_fullscreen_changed(handle, is_fullscreen))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_close(Box::new(move || {
-                app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window()));
+                app.update(|cx| cx.update_window(handle, |cx| cx.remove_window()));
             }));
         }
 
@@ -1402,31 +1355,27 @@ impl AppContext {
 
         platform_window.set_input_handler(Box::new(WindowInputHandler {
             app: self.upgrade().0,
-            window_id,
+            window: handle,
         }));
 
-        let mut window = Window::new(window_id, platform_window, self, build_root_view);
-        let mut cx = WindowContext::mutable(self, &mut window, window_id);
+        let mut window = Window::new(handle, platform_window, self, build_root_view);
+        let mut cx = WindowContext::mutable(self, &mut window, handle);
         cx.layout(false).expect("initial layout should not error");
         let scene = cx.paint().expect("initial paint should not error");
         window.platform_window.present_scene(scene);
         window
     }
 
-    pub fn replace_root_view<V, F>(
-        &mut self,
-        window_id: usize,
-        build_root_view: F,
-    ) -> Option<ViewHandle<V>>
-    where
-        V: View,
-        F: FnOnce(&mut ViewContext<V>) -> V,
-    {
-        self.update_window(window_id, |cx| cx.replace_root_view(build_root_view))
+    pub fn active_window(&self) -> Option<AnyWindowHandle> {
+        self.platform.main_window()
+    }
+
+    pub fn windows(&self) -> impl '_ + Iterator<Item = AnyWindowHandle> {
+        self.windows.keys().copied()
     }
 
     pub fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
-        if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) {
+        if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
             view.as_any().downcast_ref().expect("downcast is type safe")
         } else {
             panic!("circular view reference for type {}", type_name::<T>());
@@ -1436,7 +1385,7 @@ impl AppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         if self.ref_counts.lock().is_entity_alive(handle.view_id) {
             Some(ViewHandle::new(
-                handle.window_id,
+                handle.window,
                 handle.view_id,
                 &self.ref_counts,
             ))
@@ -1448,7 +1397,7 @@ impl AppContext {
     fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
         if self.ref_counts.lock().is_entity_alive(handle.view_id) {
             Some(AnyViewHandle::new(
-                handle.window_id,
+                handle.window,
                 handle.view_id,
                 handle.view_type,
                 self.ref_counts.clone(),
@@ -1478,13 +1427,13 @@ impl AppContext {
                     .push_back(Effect::ModelRelease { model_id, model });
             }
 
-            for (window_id, view_id) in dropped_views {
+            for (window, view_id) in dropped_views {
                 self.subscriptions.remove(view_id);
                 self.observations.remove(view_id);
-                self.views_metadata.remove(&(window_id, view_id));
-                let mut view = self.views.remove(&(window_id, view_id)).unwrap();
+                self.views_metadata.remove(&(window, view_id));
+                let mut view = self.views.remove(&(window, view_id)).unwrap();
                 view.release(self);
-                if let Some(window) = self.windows.get_mut(&window_id) {
+                if let Some(window) = self.windows.get_mut(&window) {
                     window.parents.remove(&view_id);
                     window
                         .invalidation
@@ -1512,7 +1461,7 @@ impl AppContext {
 
             let mut refreshing = false;
             let mut updated_windows = HashSet::default();
-            let mut focus_effects = HashMap::<usize, FocusEffect>::default();
+            let mut focus_effects = HashMap::<AnyWindowHandle, FocusEffect>::default();
             loop {
                 self.remove_dropped_entities();
                 if let Some(effect) = self.pending_effects.pop_front() {
@@ -1556,9 +1505,10 @@ impl AppContext {
                             observations.emit(model_id, |callback| callback(self));
                         }
 
-                        Effect::ViewNotification { window_id, view_id } => {
-                            self.handle_view_notification_effect(window_id, view_id)
-                        }
+                        Effect::ViewNotification {
+                            window: window_id,
+                            view_id,
+                        } => self.handle_view_notification_effect(window_id, view_id),
 
                         Effect::GlobalNotification { type_id } => {
                             let mut subscriptions = self.global_observations.clone();
@@ -1589,13 +1539,13 @@ impl AppContext {
 
                         Effect::Focus(mut effect) => {
                             if focus_effects
-                                .get(&effect.window_id())
+                                .get(&effect.window())
                                 .map_or(false, |prev_effect| prev_effect.is_forced())
                             {
                                 effect.force();
                             }
 
-                            focus_effects.insert(effect.window_id(), effect);
+                            focus_effects.insert(effect.window(), effect);
                         }
 
                         Effect::FocusObservation {
@@ -1610,42 +1560,38 @@ impl AppContext {
                             );
                         }
 
-                        Effect::ResizeWindow { window_id } => {
-                            if let Some(window) = self.windows.get_mut(&window_id) {
+                        Effect::ResizeWindow { window } => {
+                            if let Some(window) = self.windows.get_mut(&window) {
                                 window
                                     .invalidation
                                     .get_or_insert(WindowInvalidation::default());
                             }
-                            self.handle_window_moved(window_id);
+                            self.handle_window_moved(window);
                         }
 
-                        Effect::MoveWindow { window_id } => {
-                            self.handle_window_moved(window_id);
+                        Effect::MoveWindow { window } => {
+                            self.handle_window_moved(window);
                         }
 
                         Effect::WindowActivationObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_activation_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
 
-                        Effect::ActivateWindow {
-                            window_id,
-                            is_active,
-                        } => {
-                            if self.handle_window_activation_effect(window_id, is_active)
-                                && is_active
+                        Effect::ActivateWindow { window, is_active } => {
+                            if self.handle_window_activation_effect(window, is_active) && is_active
                             {
                                 focus_effects
-                                    .entry(window_id)
+                                    .entry(window)
                                     .or_insert_with(|| FocusEffect::View {
-                                        window_id,
+                                        window,
                                         view_id: self
-                                            .read_window(window_id, |cx| cx.focused_view_id())
+                                            .read_window(window, |cx| cx.focused_view_id())
                                             .flatten(),
                                         is_forced: true,
                                     })
@@ -1654,26 +1600,26 @@ impl AppContext {
                         }
 
                         Effect::WindowFullscreenObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_fullscreen_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
 
                         Effect::FullscreenWindow {
-                            window_id,
+                            window,
                             is_fullscreen,
-                        } => self.handle_fullscreen_effect(window_id, is_fullscreen),
+                        } => self.handle_fullscreen_effect(window, is_fullscreen),
 
                         Effect::WindowBoundsObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_bounds_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
@@ -1685,18 +1631,15 @@ impl AppContext {
                         Effect::ActionDispatchNotification { action_id } => {
                             self.handle_action_dispatch_notification_effect(action_id)
                         }
-                        Effect::WindowShouldCloseSubscription {
-                            window_id,
-                            callback,
-                        } => {
-                            self.handle_window_should_close_subscription_effect(window_id, callback)
+                        Effect::WindowShouldCloseSubscription { window, callback } => {
+                            self.handle_window_should_close_subscription_effect(window, callback)
                         }
                         Effect::Keystroke {
-                            window_id,
+                            window,
                             keystroke,
                             handled_by,
                             result,
-                        } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
+                        } => self.handle_keystroke_effect(window, keystroke, handled_by, result),
                         Effect::ActiveLabeledTasksChanged => {
                             self.handle_active_labeled_tasks_changed_effect()
                         }
@@ -1711,8 +1654,8 @@ impl AppContext {
                     }
                     self.pending_notifications.clear();
                 } else {
-                    for window_id in self.windows.keys().cloned().collect::<Vec<_>>() {
-                        self.update_window(window_id, |cx| {
+                    for window in self.windows().collect::<Vec<_>>() {
+                        self.update_window(window, |cx| {
                             let invalidation = if refreshing {
                                 let mut invalidation =
                                     cx.window.invalidation.take().unwrap_or_default();
@@ -1728,7 +1671,7 @@ impl AppContext {
                                 let appearance = cx.window.platform_window.appearance();
                                 cx.invalidate(invalidation, appearance);
                                 if let Some(old_parents) = cx.layout(refreshing).log_err() {
-                                    updated_windows.insert(window_id);
+                                    updated_windows.insert(window);
 
                                     if let Some(focused_view_id) = cx.focused_view_id() {
                                         let old_ancestors = std::iter::successors(
@@ -1743,15 +1686,14 @@ impl AppContext {
                                         for old_ancestor in old_ancestors.iter().copied() {
                                             if !new_ancestors.contains(&old_ancestor) {
                                                 if let Some(mut view) =
-                                                    cx.views.remove(&(window_id, old_ancestor))
+                                                    cx.views.remove(&(window, old_ancestor))
                                                 {
                                                     view.focus_out(
                                                         focused_view_id,
                                                         cx,
                                                         old_ancestor,
                                                     );
-                                                    cx.views
-                                                        .insert((window_id, old_ancestor), view);
+                                                    cx.views.insert((window, old_ancestor), view);
                                                 }
                                             }
                                         }
@@ -1760,15 +1702,14 @@ impl AppContext {
                                         for new_ancestor in new_ancestors.iter().copied() {
                                             if !old_ancestors.contains(&new_ancestor) {
                                                 if let Some(mut view) =
-                                                    cx.views.remove(&(window_id, new_ancestor))
+                                                    cx.views.remove(&(window, new_ancestor))
                                                 {
                                                     view.focus_in(
                                                         focused_view_id,
                                                         cx,
                                                         new_ancestor,
                                                     );
-                                                    cx.views
-                                                        .insert((window_id, new_ancestor), view);
+                                                    cx.views.insert((window, new_ancestor), view);
                                                 }
                                             }
                                         }
@@ -1777,13 +1718,13 @@ impl AppContext {
                                         // there isn't any pending focus, focus the root view.
                                         let root_view_id = cx.window.root_view().id();
                                         if focused_view_id != root_view_id
-                                            && !cx.views.contains_key(&(window_id, focused_view_id))
-                                            && !focus_effects.contains_key(&window_id)
+                                            && !cx.views.contains_key(&(window, focused_view_id))
+                                            && !focus_effects.contains_key(&window)
                                         {
                                             focus_effects.insert(
-                                                window_id,
+                                                window,
                                                 FocusEffect::View {
-                                                    window_id,
+                                                    window,
                                                     view_id: Some(root_view_id),
                                                     is_forced: false,
                                                 },
@@ -1804,8 +1745,8 @@ impl AppContext {
                             callback(self);
                         }
 
-                        for window_id in updated_windows.drain() {
-                            self.update_window(window_id, |cx| {
+                        for window in updated_windows.drain() {
+                            self.update_window(window, |cx| {
                                 if let Some(scene) = cx.paint().log_err() {
                                     cx.window.platform_window.present_scene(scene);
                                 }
@@ -1826,39 +1767,37 @@ impl AppContext {
         }
     }
 
-    fn window_was_resized(&mut self, window_id: usize) {
+    fn window_was_resized(&mut self, window: AnyWindowHandle) {
         self.pending_effects
-            .push_back(Effect::ResizeWindow { window_id });
+            .push_back(Effect::ResizeWindow { window });
     }
 
-    fn window_was_moved(&mut self, window_id: usize) {
+    fn window_was_moved(&mut self, window: AnyWindowHandle) {
         self.pending_effects
-            .push_back(Effect::MoveWindow { window_id });
+            .push_back(Effect::MoveWindow { window });
     }
 
-    fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) {
+    fn window_was_fullscreen_changed(&mut self, window: AnyWindowHandle, is_fullscreen: bool) {
         self.pending_effects.push_back(Effect::FullscreenWindow {
-            window_id,
+            window,
             is_fullscreen,
         });
     }
 
-    fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) {
-        self.pending_effects.push_back(Effect::ActivateWindow {
-            window_id,
-            is_active,
-        });
+    fn window_changed_active_status(&mut self, window: AnyWindowHandle, is_active: bool) {
+        self.pending_effects
+            .push_back(Effect::ActivateWindow { window, is_active });
     }
 
     fn keystroke(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         keystroke: Keystroke,
         handled_by: Option<Box<dyn Action>>,
         result: MatchResult,
     ) {
         self.pending_effects.push_back(Effect::Keystroke {
-            window_id,
+            window,
             keystroke,
             handled_by,
             result,
@@ -1881,16 +1820,16 @@ impl AppContext {
 
     fn handle_view_notification_effect(
         &mut self,
-        observed_window_id: usize,
+        observed_window: AnyWindowHandle,
         observed_view_id: usize,
     ) {
-        let view_key = (observed_window_id, observed_view_id);
+        let view_key = (observed_window, observed_view_id);
         if let Some((view, mut view_metadata)) = self
             .views
             .remove(&view_key)
             .zip(self.views_metadata.remove(&view_key))
         {
-            if let Some(window) = self.windows.get_mut(&observed_window_id) {
+            if let Some(window) = self.windows.get_mut(&observed_window) {
                 window
                     .invalidation
                     .get_or_insert_with(Default::default)
@@ -1917,17 +1856,17 @@ impl AppContext {
             })
     }
 
-    fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) {
-        self.update_window(window_id, |cx| {
+    fn handle_fullscreen_effect(&mut self, window: AnyWindowHandle, is_fullscreen: bool) {
+        self.update_window(window, |cx| {
             cx.window.is_fullscreen = is_fullscreen;
 
             let mut fullscreen_observations = cx.window_fullscreen_observations.clone();
-            fullscreen_observations.emit(window_id, |callback| callback(is_fullscreen, cx));
+            fullscreen_observations.emit(window, |callback| callback(is_fullscreen, cx));
 
             if let Some(uuid) = cx.window_display_uuid() {
                 let bounds = cx.window_bounds();
                 let mut bounds_observations = cx.window_bounds_observations.clone();
-                bounds_observations.emit(window_id, |callback| callback(bounds, uuid, cx));
+                bounds_observations.emit(window, |callback| callback(bounds, uuid, cx));
             }
 
             Some(())
@@ -1936,42 +1875,42 @@ impl AppContext {
 
     fn handle_keystroke_effect(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         keystroke: Keystroke,
         handled_by: Option<Box<dyn Action>>,
         result: MatchResult,
     ) {
-        self.update_window(window_id, |cx| {
+        self.update_window(window, |cx| {
             let mut observations = cx.keystroke_observations.clone();
-            observations.emit(window_id, move |callback| {
+            observations.emit(window, move |callback| {
                 callback(&keystroke, &result, handled_by.as_ref(), cx)
             });
         });
     }
 
-    fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) -> bool {
-        self.update_window(window_id, |cx| {
+    fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
+        self.update_window(window, |cx| {
             if cx.window.is_active == active {
                 return false;
             }
             cx.window.is_active = active;
 
             let mut observations = cx.window_activation_observations.clone();
-            observations.emit(window_id, |callback| callback(active, cx));
+            observations.emit(window, |callback| callback(active, cx));
             true
         })
         .unwrap_or(false)
     }
 
     fn handle_focus_effect(&mut self, effect: FocusEffect) {
-        let window_id = effect.window_id();
-        self.update_window(window_id, |cx| {
+        let window = effect.window();
+        self.update_window(window, |cx| {
             // Ensure the newly-focused view still exists, otherwise focus
             // the root view instead.
             let focused_id = match effect {
                 FocusEffect::View { view_id, .. } => {
                     if let Some(view_id) = view_id {
-                        if cx.views.contains_key(&(window_id, view_id)) {
+                        if cx.views.contains_key(&(window, view_id)) {
                             Some(view_id)
                         } else {
                             Some(cx.root_view().id())
@@ -1996,9 +1935,9 @@ impl AppContext {
             if focus_changed {
                 if let Some(blurred_id) = blurred_id {
                     for view_id in cx.ancestors(blurred_id).collect::<Vec<_>>() {
-                        if let Some(mut view) = cx.views.remove(&(window_id, view_id)) {
+                        if let Some(mut view) = cx.views.remove(&(window, view_id)) {
                             view.focus_out(blurred_id, cx, view_id);
-                            cx.views.insert((window_id, view_id), view);
+                            cx.views.insert((window, view_id), view);
                         }
                     }
 
@@ -2010,9 +1949,9 @@ impl AppContext {
             if focus_changed || effect.is_forced() {
                 if let Some(focused_id) = focused_id {
                     for view_id in cx.ancestors(focused_id).collect::<Vec<_>>() {
-                        if let Some(mut view) = cx.views.remove(&(window_id, view_id)) {
+                        if let Some(mut view) = cx.views.remove(&(window, view_id)) {
                             view.focus_in(focused_id, cx, view_id);
-                            cx.views.insert((window_id, view_id), view);
+                            cx.views.insert((window, view_id), view);
                         }
                     }
 
@@ -2034,24 +1973,24 @@ impl AppContext {
 
     fn handle_window_should_close_subscription_effect(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         mut callback: WindowShouldCloseSubscriptionCallback,
     ) {
         let mut app = self.upgrade();
-        if let Some(window) = self.windows.get_mut(&window_id) {
+        if let Some(window) = self.windows.get_mut(&window) {
             window
                 .platform_window
                 .on_should_close(Box::new(move || app.update(|cx| callback(cx))))
         }
     }
 
-    fn handle_window_moved(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| {
+    fn handle_window_moved(&mut self, window: AnyWindowHandle) {
+        self.update_window(window, |cx| {
             if let Some(display) = cx.window_display_uuid() {
                 let bounds = cx.window_bounds();
                 cx.window_bounds_observations
                     .clone()
-                    .emit(window_id, move |callback| {
+                    .emit(window, move |callback| {
                         callback(bounds, display, cx);
                         true
                     });
@@ -2068,10 +2007,10 @@ impl AppContext {
             });
     }
 
-    pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
+    pub fn focus(&mut self, window: AnyWindowHandle, view_id: Option<usize>) {
         self.pending_effects
             .push_back(Effect::Focus(FocusEffect::View {
-                window_id,
+                window,
                 view_id,
                 is_forced: false,
             }));
@@ -2153,6 +2092,46 @@ impl BorrowAppContext for AppContext {
     }
 }
 
+impl BorrowWindowContext for AppContext {
+    type Result<T> = Option<T>;
+
+    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T,
+    {
+        AppContext::read_window(self, window, f)
+    }
+
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        AppContext::read_window(self, window, f).flatten()
+    }
+
+    fn update_window<T, F>(&mut self, handle: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
+        self.update(|cx| {
+            let mut window = cx.windows.remove(&handle)?;
+            let mut window_context = WindowContext::mutable(cx, &mut window, handle);
+            let result = f(&mut window_context);
+            if !window_context.removed {
+                cx.windows.insert(handle, window);
+            }
+            Some(result)
+        })
+    }
+
+    fn update_window_optional<T, F>(&mut self, handle: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        AppContext::update_window(self, handle, f).flatten()
+    }
+}
+
 #[derive(Debug)]
 pub enum ParentId {
     View(usize),

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

@@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
         let cx = app.0.clone();
         move |action| {
             let mut cx = cx.borrow_mut();
-            if let Some(main_window_id) = cx.platform.main_window_id() {
-                let dispatched = cx
-                    .update_window(main_window_id, |cx| {
+            if let Some(main_window) = cx.active_window() {
+                let dispatched = main_window
+                    .update(&mut *cx, |cx| {
                         if let Some(view_id) = cx.focused_view_id() {
                             cx.dispatch_action(Some(view_id), action);
                             true

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

@@ -9,7 +9,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
 
 #[cfg(any(test, feature = "test-support"))]
 use crate::util::post_inc;
-use crate::ElementStateId;
+use crate::{AnyWindowHandle, ElementStateId};
 
 lazy_static! {
     static ref LEAK_BACKTRACE: bool =
@@ -26,7 +26,7 @@ pub struct RefCounts {
     entity_counts: HashMap<usize, usize>,
     element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
     dropped_models: HashSet<usize>,
-    dropped_views: HashSet<(usize, usize)>,
+    dropped_views: HashSet<(AnyWindowHandle, usize)>,
     dropped_element_states: HashSet<ElementStateId>,
 
     #[cfg(any(test, feature = "test-support"))]
@@ -55,12 +55,12 @@ impl RefCounts {
         }
     }
 
-    pub fn inc_view(&mut self, window_id: usize, view_id: usize) {
+    pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         match self.entity_counts.entry(view_id) {
             Entry::Occupied(mut entry) => *entry.get_mut() += 1,
             Entry::Vacant(entry) => {
                 entry.insert(1);
-                self.dropped_views.remove(&(window_id, view_id));
+                self.dropped_views.remove(&(window, view_id));
             }
         }
     }
@@ -94,12 +94,12 @@ impl RefCounts {
         }
     }
 
-    pub fn dec_view(&mut self, window_id: usize, view_id: usize) {
+    pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         let count = self.entity_counts.get_mut(&view_id).unwrap();
         *count -= 1;
         if *count == 0 {
             self.entity_counts.remove(&view_id);
-            self.dropped_views.insert((window_id, view_id));
+            self.dropped_views.insert((window, view_id));
         }
     }
 
@@ -120,7 +120,7 @@ impl RefCounts {
         &mut self,
     ) -> (
         HashSet<usize>,
-        HashSet<(usize, usize)>,
+        HashSet<(AnyWindowHandle, usize)>,
         HashSet<ElementStateId>,
     ) {
         (

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

@@ -4,9 +4,9 @@ use crate::{
     keymap_matcher::{Binding, Keystroke},
     platform,
     platform::{Event, InputHandler, KeyDownEvent, Platform},
-    Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
-    ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
-    WindowContext,
+    Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
+    Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakHandle, WindowContext, WindowHandle,
 };
 use collections::BTreeMap;
 use futures::Future;
@@ -60,7 +60,7 @@ impl TestAppContext {
             RefCounts::new(leak_detector),
             (),
         );
-        cx.next_entity_id = first_entity_id;
+        cx.next_id = first_entity_id;
         let cx = TestAppContext {
             cx: Rc::new(RefCell::new(cx)),
             foreground_platform,
@@ -72,8 +72,8 @@ impl TestAppContext {
         cx
     }
 
-    pub fn dispatch_action<A: Action>(&mut self, window_id: usize, action: A) {
-        self.update_window(window_id, |window| {
+    pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
+        self.update_window(window, |window| {
             window.dispatch_action(window.focused_view_id(), &action);
         })
         .expect("window not found");
@@ -81,10 +81,10 @@ impl TestAppContext {
 
     pub fn available_actions(
         &self,
-        window_id: usize,
+        window: AnyWindowHandle,
         view_id: usize,
     ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        self.read_window(window_id, |cx| cx.available_actions(view_id))
+        self.read_window(window, |cx| cx.available_actions(view_id))
             .unwrap_or_default()
     }
 
@@ -92,33 +92,34 @@ impl TestAppContext {
         self.update(|cx| cx.dispatch_global_action_any(&action));
     }
 
-    pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
-        let handled = self
-            .cx
-            .borrow_mut()
-            .update_window(window_id, |cx| {
-                if cx.dispatch_keystroke(&keystroke) {
-                    return true;
-                }
+    pub fn dispatch_keystroke(
+        &mut self,
+        window: AnyWindowHandle,
+        keystroke: Keystroke,
+        is_held: bool,
+    ) {
+        let handled = window.update(self, |cx| {
+            if cx.dispatch_keystroke(&keystroke) {
+                return true;
+            }
 
-                if cx.dispatch_event(
-                    Event::KeyDown(KeyDownEvent {
-                        keystroke: keystroke.clone(),
-                        is_held,
-                    }),
-                    false,
-                ) {
-                    return true;
-                }
+            if cx.dispatch_event(
+                Event::KeyDown(KeyDownEvent {
+                    keystroke: keystroke.clone(),
+                    is_held,
+                }),
+                false,
+            ) {
+                return true;
+            }
 
-                false
-            })
-            .unwrap_or(false);
+            false
+        });
 
         if !handled && !keystroke.cmd && !keystroke.ctrl {
             WindowInputHandler {
                 app: self.cx.clone(),
-                window_id,
+                window,
             }
             .replace_text_in_range(None, &keystroke.key)
         }
@@ -126,18 +127,18 @@ impl TestAppContext {
 
     pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
         &self,
-        window_id: usize,
+        window: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        self.cx.borrow().read_window(window_id, callback)
+        self.cx.borrow().read_window(window, callback)
     }
 
     pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        self.cx.borrow_mut().update_window(window_id, callback)
+        self.cx.borrow_mut().update_window(window, callback)
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -148,26 +149,17 @@ impl TestAppContext {
         self.cx.borrow_mut().add_model(build_model)
     }
 
-    pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
+    pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
     where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> T,
+        V: View,
+        F: FnOnce(&mut ViewContext<V>) -> V,
     {
-        let (window_id, view) = self
+        let window = self
             .cx
             .borrow_mut()
             .add_window(Default::default(), build_root_view);
-        self.simulate_window_activation(Some(window_id));
-        (window_id, view)
-    }
-
-    pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
-    where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> T,
-    {
-        self.update_window(window_id, |cx| cx.add_view(build_view))
-            .expect("window not found")
+        window.simulate_activation(self);
+        window
     }
 
     pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
@@ -190,8 +182,8 @@ impl TestAppContext {
         self.cx.borrow_mut().subscribe_global(callback)
     }
 
-    pub fn window_ids(&self) -> Vec<usize> {
-        self.cx.borrow().windows.keys().copied().collect()
+    pub fn windows(&self) -> Vec<AnyWindowHandle> {
+        self.cx.borrow().windows().collect()
     }
 
     pub fn remove_all_windows(&mut self) {
@@ -261,76 +253,6 @@ impl TestAppContext {
         self.foreground_platform.as_ref().did_prompt_for_new_path()
     }
 
-    pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
-        use postage::prelude::Sink as _;
-
-        let mut done_tx = self
-            .platform_window_mut(window_id)
-            .pending_prompts
-            .borrow_mut()
-            .pop_front()
-            .expect("prompt was not called");
-        done_tx.try_send(answer).ok();
-    }
-
-    pub fn has_pending_prompt(&self, window_id: usize) -> bool {
-        let window = self.platform_window_mut(window_id);
-        let prompts = window.pending_prompts.borrow_mut();
-        !prompts.is_empty()
-    }
-
-    pub fn current_window_title(&self, window_id: usize) -> Option<String> {
-        self.platform_window_mut(window_id).title.clone()
-    }
-
-    pub fn simulate_window_close(&self, window_id: usize) -> bool {
-        let handler = self
-            .platform_window_mut(window_id)
-            .should_close_handler
-            .take();
-        if let Some(mut handler) = handler {
-            let should_close = handler();
-            self.platform_window_mut(window_id).should_close_handler = Some(handler);
-            should_close
-        } else {
-            false
-        }
-    }
-
-    pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
-        let mut window = self.platform_window_mut(window_id);
-        window.size = size;
-        let mut handlers = mem::take(&mut window.resize_handlers);
-        drop(window);
-        for handler in &mut handlers {
-            handler();
-        }
-        self.platform_window_mut(window_id).resize_handlers = handlers;
-    }
-
-    pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
-        self.cx.borrow_mut().update(|cx| {
-            let other_window_ids = cx
-                .windows
-                .keys()
-                .filter(|window_id| Some(**window_id) != to_activate)
-                .copied()
-                .collect::<Vec<_>>();
-
-            for window_id in other_window_ids {
-                cx.window_changed_active_status(window_id, false)
-            }
-
-            if let Some(to_activate) = to_activate {
-                cx.window_changed_active_status(to_activate, true)
-            }
-        });
-    }
-
-    pub fn is_window_edited(&self, window_id: usize) -> bool {
-        self.platform_window_mut(window_id).edited
-    }
-
     pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
         self.cx.borrow().leak_detector()
     }
@@ -351,18 +273,6 @@ impl TestAppContext {
         self.assert_dropped(weak);
     }
 
-    fn platform_window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
-        std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
-            let window = state.windows.get_mut(&window_id).unwrap();
-            let test_window = window
-                .platform_window
-                .as_any_mut()
-                .downcast_mut::<platform::test::Window>()
-                .unwrap();
-            test_window
-        })
-    }
-
     pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
         self.condition_duration = duration;
     }
@@ -405,19 +315,39 @@ impl BorrowAppContext for TestAppContext {
 }
 
 impl BorrowWindowContext for TestAppContext {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+    type Result<T> = T;
+
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
         self.cx
             .borrow()
-            .read_window(window_id, f)
+            .read_window(window, f)
             .expect("window was closed")
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::read_window(self, window, f)
+    }
+
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window: AnyWindowHandle,
+        f: F,
+    ) -> T {
         self.cx
             .borrow_mut()
-            .update_window(window_id, f)
+            .update_window(window, f)
             .expect("window was closed")
     }
+
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::update_window(self, window, f)
+    }
 }
 
 impl<T: Entity> ModelHandle<T> {
@@ -532,6 +462,71 @@ impl<T: Entity> ModelHandle<T> {
     }
 }
 
+impl AnyWindowHandle {
+    pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
+        let window = self.platform_window_mut(cx);
+        let prompts = window.pending_prompts.borrow_mut();
+        !prompts.is_empty()
+    }
+
+    pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
+        self.platform_window_mut(cx).title.clone()
+    }
+
+    pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
+        let handler = self.platform_window_mut(cx).should_close_handler.take();
+        if let Some(mut handler) = handler {
+            let should_close = handler();
+            self.platform_window_mut(cx).should_close_handler = Some(handler);
+            should_close
+        } else {
+            false
+        }
+    }
+
+    pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
+        let mut window = self.platform_window_mut(cx);
+        window.size = size;
+        let mut handlers = mem::take(&mut window.resize_handlers);
+        drop(window);
+        for handler in &mut handlers {
+            handler();
+        }
+        self.platform_window_mut(cx).resize_handlers = handlers;
+    }
+
+    pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
+        self.platform_window_mut(cx).edited
+    }
+
+    pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
+        use postage::prelude::Sink as _;
+
+        let mut done_tx = self
+            .platform_window_mut(cx)
+            .pending_prompts
+            .borrow_mut()
+            .pop_front()
+            .expect("prompt was not called");
+        done_tx.try_send(answer).ok();
+    }
+
+    fn platform_window_mut<'a>(
+        &self,
+        cx: &'a mut TestAppContext,
+    ) -> std::cell::RefMut<'a, platform::test::Window> {
+        std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
+            let window = state.windows.get_mut(&self).unwrap();
+            let test_window = window
+                .platform_window
+                .as_any_mut()
+                .downcast_mut::<platform::test::Window>()
+                .unwrap();
+            test_window
+        })
+    }
+}
+
 impl<T: View> ViewHandle<T> {
     pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
         use postage::prelude::{Sink as _, Stream as _};

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

@@ -13,9 +13,10 @@ use crate::{
     },
     text_layout::TextLayoutCache,
     util::post_inc,
-    Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, PaintContext, SceneBuilder,
-    Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
+    Action, AnyView, AnyViewHandle, AnyWindowHandle, AppContext, BorrowAppContext,
+    BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion,
+    MouseRegionId, PaintContext, SceneBuilder, Subscription, View, ViewContext, ViewHandle,
+    WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -60,7 +61,7 @@ pub struct Window {
 
 impl Window {
     pub fn new<V, F>(
-        window_id: usize,
+        handle: AnyWindowHandle,
         platform_window: Box<dyn platform::Window>,
         cx: &mut AppContext,
         build_view: F,
@@ -92,7 +93,7 @@ impl Window {
             appearance,
         };
 
-        let mut window_context = WindowContext::mutable(cx, &mut window, window_id);
+        let mut window_context = WindowContext::mutable(cx, &mut window, handle);
         let root_view = window_context.add_view(|cx| build_view(cx));
         if let Some(invalidation) = window_context.window.invalidation.take() {
             window_context.invalidate(invalidation, appearance);
@@ -113,7 +114,7 @@ impl Window {
 pub struct WindowContext<'a> {
     pub(crate) app_context: Reference<'a, AppContext>,
     pub(crate) window: Reference<'a, Window>,
-    pub(crate) window_id: usize,
+    pub(crate) window_handle: AnyWindowHandle,
     pub(crate) removed: bool,
 }
 
@@ -142,42 +143,66 @@ impl BorrowAppContext for WindowContext<'_> {
 }
 
 impl BorrowWindowContext for WindowContext<'_> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
-        if self.window_id == window_id {
+    type Result<T> = T;
+
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, handle: AnyWindowHandle, f: F) -> T {
+        if self.window_handle == handle {
             f(self)
         } else {
             panic!("read_with called with id of window that does not belong to this context")
         }
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
-        if self.window_id == window_id {
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::read_window(self, window, f)
+    }
+
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        handle: AnyWindowHandle,
+        f: F,
+    ) -> T {
+        if self.window_handle == handle {
             f(self)
         } else {
             panic!("update called with id of window that does not belong to this context")
         }
     }
+
+    fn update_window_optional<T, F>(&mut self, handle: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::update_window(self, handle, f)
+    }
 }
 
 impl<'a> WindowContext<'a> {
     pub fn mutable(
         app_context: &'a mut AppContext,
         window: &'a mut Window,
-        window_id: usize,
+        handle: AnyWindowHandle,
     ) -> Self {
         Self {
             app_context: Reference::Mutable(app_context),
             window: Reference::Mutable(window),
-            window_id,
+            window_handle: handle,
             removed: false,
         }
     }
 
-    pub fn immutable(app_context: &'a AppContext, window: &'a Window, window_id: usize) -> Self {
+    pub fn immutable(
+        app_context: &'a AppContext,
+        window: &'a Window,
+        handle: AnyWindowHandle,
+    ) -> Self {
         Self {
             app_context: Reference::Immutable(app_context),
             window: Reference::Immutable(window),
-            window_id,
+            window_handle: handle,
             removed: false,
         }
     }
@@ -186,8 +211,8 @@ impl<'a> WindowContext<'a> {
         self.removed = true;
     }
 
-    pub fn window_id(&self) -> usize {
-        self.window_id
+    pub fn window(&self) -> AnyWindowHandle {
+        self.window_handle
     }
 
     pub fn app_context(&mut self) -> &mut AppContext {
@@ -210,10 +235,10 @@ impl<'a> WindowContext<'a> {
     where
         F: FnOnce(&mut dyn AnyView, &mut Self) -> T,
     {
-        let window_id = self.window_id;
-        let mut view = self.views.remove(&(window_id, view_id))?;
+        let handle = self.window_handle;
+        let mut view = self.views.remove(&(handle, view_id))?;
         let result = f(view.as_mut(), self);
-        self.views.insert((window_id, view_id), view);
+        self.views.insert((handle, view_id), view);
         Some(result)
     }
 
@@ -238,9 +263,9 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut WindowContext)) {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         self.app_context.defer(move |cx| {
-            cx.update_window(window_id, |cx| callback(cx));
+            cx.update_window(handle, |cx| callback(cx));
         })
     }
 
@@ -280,10 +305,10 @@ impl<'a> WindowContext<'a> {
         H: Handle<E>,
         F: 'static + FnMut(H, &E::Event, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window_handle = self.window_handle;
         self.app_context
             .subscribe_internal(handle, move |emitter, event, cx| {
-                cx.update_window(window_id, |cx| callback(emitter, event, cx))
+                cx.update_window(window_handle, |cx| callback(emitter, event, cx))
                     .unwrap_or(false)
             })
     }
@@ -292,17 +317,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(bool, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowActivationObservation {
-                window_id,
+                window: handle,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowActivationObservation(
             self.window_activation_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(handle, subscription_id),
         )
     }
 
@@ -310,17 +335,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(bool, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowFullscreenObservation {
-                window_id,
+                window,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowActivationObservation(
             self.window_activation_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -328,17 +353,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(WindowBounds, Uuid, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowBoundsObservation {
-                window_id,
+                window,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowBoundsObservation(
             self.window_bounds_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -347,13 +372,13 @@ impl<'a> WindowContext<'a> {
         F: 'static
             + FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.keystroke_observations
-            .add_callback(window_id, subscription_id, Box::new(callback));
+            .add_callback(window, subscription_id, Box::new(callback));
         Subscription::KeystrokeObservation(
             self.keystroke_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -361,11 +386,11 @@ impl<'a> WindowContext<'a> {
         &self,
         view_id: usize,
     ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let mut contexts = Vec::new();
         let mut handler_depths_by_action_id = HashMap::<TypeId, usize>::default();
         for (depth, view_id) in self.ancestors(view_id).enumerate() {
-            if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
+            if let Some(view_metadata) = self.views_metadata.get(&(handle, view_id)) {
                 contexts.push(view_metadata.keymap_context.clone());
                 if let Some(actions) = self.actions.get(&view_metadata.type_id) {
                     handler_depths_by_action_id
@@ -410,13 +435,13 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.focused_view_id() {
             let dispatch_path = self
                 .ancestors(focused_view_id)
                 .filter_map(|view_id| {
                     self.views_metadata
-                        .get(&(window_id, view_id))
+                        .get(&(handle, view_id))
                         .map(|view| (view_id, view.keymap_context.clone()))
                 })
                 .collect();
@@ -441,15 +466,10 @@ impl<'a> WindowContext<'a> {
                 }
             };
 
-            self.keystroke(
-                window_id,
-                keystroke.clone(),
-                handled_by,
-                match_result.clone(),
-            );
+            self.keystroke(handle, keystroke.clone(), handled_by, match_result.clone());
             keystroke_handled
         } else {
-            self.keystroke(window_id, keystroke.clone(), None, MatchResult::None);
+            self.keystroke(handle, keystroke.clone(), None, MatchResult::None);
             false
         }
     }
@@ -457,7 +477,7 @@ impl<'a> WindowContext<'a> {
     pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
         let mut mouse_events = SmallVec::<[_; 2]>::new();
         let mut notified_views: HashSet<usize> = Default::default();
-        let window_id = self.window_id;
+        let handle = self.window_handle;
 
         // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
         //    get mapped into the mouse-specific MouseEvent type.
@@ -518,6 +538,18 @@ impl<'a> WindowContext<'a> {
                 // NOTE: The order of event pushes is important! MouseUp events MUST be fired
                 // before click events, and so the MouseUp events need to be pushed before
                 // MouseClick events.
+
+                // Synthesize one last drag event to end the drag
+                mouse_events.push(MouseEvent::Drag(MouseDrag {
+                    region: Default::default(),
+                    prev_mouse_position: self.window.mouse_position,
+                    platform_event: MouseMovedEvent {
+                        position: e.position,
+                        pressed_button: Some(e.button),
+                        modifiers: e.modifiers,
+                    },
+                    end: true,
+                }));
                 mouse_events.push(MouseEvent::Up(MouseUp {
                     region: Default::default(),
                     platform_event: e.clone(),
@@ -565,8 +597,16 @@ impl<'a> WindowContext<'a> {
                             region: Default::default(),
                             prev_mouse_position: self.window.mouse_position,
                             platform_event: e.clone(),
+                            end: false,
                         }));
                     } else if let Some((_, clicked_button)) = self.window.clicked_region {
+                        mouse_events.push(MouseEvent::Drag(MouseDrag {
+                            region: Default::default(),
+                            prev_mouse_position: self.window.mouse_position,
+                            platform_event: e.clone(),
+                            end: true,
+                        }));
+
                         // Mouse up event happened outside the current window. Simulate mouse up button event
                         let button_event = e.to_button_event(clicked_button);
                         mouse_events.push(MouseEvent::Up(MouseUp {
@@ -801,19 +841,19 @@ impl<'a> WindowContext<'a> {
         }
 
         for view_id in notified_views {
-            self.notify_view(window_id, view_id);
+            self.notify_view(handle, view_id);
         }
 
         any_event_handled
     }
 
     pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.key_down(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -827,12 +867,12 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.key_up(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -846,12 +886,12 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.modifiers_changed(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -886,14 +926,14 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let view_id = params.view_id;
         let mut view = self
             .views
-            .remove(&(window_id, view_id))
+            .remove(&(handle, view_id))
             .ok_or_else(|| anyhow!("view not found"))?;
         let element = view.render(self, view_id);
-        self.views.insert((window_id, view_id), view);
+        self.views.insert((handle, view_id), view);
         Ok(element)
     }
 
@@ -921,9 +961,9 @@ impl<'a> WindowContext<'a> {
                 } else if old_parent_id == new_parent_id {
                     current_view_id = *old_parent_id.unwrap();
                 } else {
-                    let window_id = self.window_id;
+                    let handle = self.window_handle;
                     for view_id_to_notify in view_ids_to_notify {
-                        self.notify_view(window_id, view_id_to_notify);
+                        self.notify_view(handle, view_id_to_notify);
                     }
                     break;
                 }
@@ -1091,7 +1131,7 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn focus(&mut self, view_id: Option<usize>) {
-        self.app_context.focus(self.window_id, view_id);
+        self.app_context.focus(self.window_handle, view_id);
     }
 
     pub fn window_bounds(&self) -> WindowBounds {
@@ -1131,17 +1171,6 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.prompt(level, msg, answers)
     }
 
-    pub fn replace_root_view<V, F>(&mut self, build_root_view: F) -> ViewHandle<V>
-    where
-        V: View,
-        F: FnOnce(&mut ViewContext<V>) -> V,
-    {
-        let root_view = self.add_view(|cx| build_root_view(cx));
-        self.window.root_view = Some(root_view.clone().into_any());
-        self.window.focused_view_id = Some(root_view.id());
-        root_view
-    }
-
     pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
     where
         T: View,
@@ -1155,26 +1184,26 @@ impl<'a> WindowContext<'a> {
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> Option<T>,
     {
-        let window_id = self.window_id;
-        let view_id = post_inc(&mut self.next_entity_id);
+        let handle = self.window_handle;
+        let view_id = post_inc(&mut self.next_id);
         let mut cx = ViewContext::mutable(self, view_id);
         let handle = if let Some(view) = build_view(&mut cx) {
             let mut keymap_context = KeymapContext::default();
             view.update_keymap_context(&mut keymap_context, cx.app_context());
             self.views_metadata.insert(
-                (window_id, view_id),
+                (handle, view_id),
                 ViewMetadata {
                     type_id: TypeId::of::<T>(),
                     keymap_context,
                 },
             );
-            self.views.insert((window_id, view_id), Box::new(view));
+            self.views.insert((handle, view_id), Box::new(view));
             self.window
                 .invalidation
                 .get_or_insert_with(Default::default)
                 .updated
                 .insert(view_id);
-            Some(ViewHandle::new(window_id, view_id, &self.ref_counts))
+            Some(ViewHandle::new(handle, view_id, &self.ref_counts))
         } else {
             None
         };
@@ -1351,7 +1380,7 @@ pub struct ChildView {
 
 impl ChildView {
     pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self {
-        let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap();
+        let view_name = cx.view_ui_name(view.window, view.id()).unwrap();
         Self {
             view_id: view.id(),
             view_name,

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

@@ -2,11 +2,11 @@ use std::{cell::RefCell, ops::Range, rc::Rc};
 
 use pathfinder_geometry::rect::RectF;
 
-use crate::{platform::InputHandler, window::WindowContext, AnyView, AppContext};
+use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext};
 
 pub struct WindowInputHandler {
     pub app: Rc<RefCell<AppContext>>,
-    pub window_id: usize,
+    pub window: AnyWindowHandle,
 }
 
 impl WindowInputHandler {
@@ -21,13 +21,12 @@ impl WindowInputHandler {
         //
         // See https://github.com/zed-industries/community/issues/444
         let mut app = self.app.try_borrow_mut().ok()?;
-        app.update_window(self.window_id, |cx| {
+        self.window.update_optional(&mut *app, |cx| {
             let view_id = cx.window.focused_view_id?;
-            let view = cx.views.get(&(self.window_id, view_id))?;
+            let view = cx.views.get(&(self.window, view_id))?;
             let result = f(view.as_ref(), &cx);
             Some(result)
         })
-        .flatten()
     }
 
     fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
@@ -35,11 +34,12 @@ impl WindowInputHandler {
         F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T,
     {
         let mut app = self.app.try_borrow_mut().ok()?;
-        app.update_window(self.window_id, |cx| {
-            let view_id = cx.window.focused_view_id?;
-            cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
-        })
-        .flatten()
+        self.window
+            .update(&mut *app, |cx| {
+                let view_id = cx.window.focused_view_id?;
+                cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
+            })
+            .flatten()
     }
 }
 
@@ -83,9 +83,8 @@ impl InputHandler for WindowInputHandler {
     }
 
     fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
-        self.app
-            .borrow()
-            .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
-            .flatten()
+        self.window.read_optional_with(&*self.app.borrow(), |cx| {
+            cx.rect_for_text_range(range_utf16)
+        })
     }
 }

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

@@ -147,6 +147,9 @@ impl<V: View> Element<V> for Resizable<V> {
                 let max_size = side.relevant_component(constraint.max);
                 let on_resize = self.on_resize.clone();
                 move |event, view: &mut V, cx| {
+                    if event.end {
+                        return;
+                    }
                     let new_size = min_size
                         .max(prev_size + side.compute_delta(event))
                         .min(max_size)

crates/gpui/src/platform.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
     },
     keymap_matcher::KeymapMatcher,
     text_layout::{LineLayout, RunStyle},
-    Action, ClipboardItem, Menu, Scene,
+    Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
 };
 use anyhow::{anyhow, bail, Result};
 use async_task::Runnable;
@@ -58,13 +58,13 @@ pub trait Platform: Send + Sync {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn Window>;
-    fn main_window_id(&self) -> Option<usize>;
+    fn main_window(&self) -> Option<AnyWindowHandle>;
 
-    fn add_status_item(&self, id: usize) -> Box<dyn Window>;
+    fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn Window>;
 
     fn write_to_clipboard(&self, item: ClipboardItem);
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;

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

@@ -21,7 +21,7 @@ pub use fonts::FontSystem;
 use platform::{MacForegroundPlatform, MacPlatform};
 pub use renderer::Surface;
 use std::{ops::Range, rc::Rc, sync::Arc};
-use window::Window;
+use window::MacWindow;
 
 use crate::executor;
 

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

@@ -1,12 +1,12 @@
 use super::{
     event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher,
-    FontSystem, Window,
+    FontSystem, MacWindow,
 };
 use crate::{
     executor,
     keymap_matcher::KeymapMatcher,
     platform::{self, AppVersion, CursorStyle, Event},
-    Action, ClipboardItem, Menu, MenuItem,
+    Action, AnyWindowHandle, ClipboardItem, Menu, MenuItem,
 };
 use anyhow::{anyhow, Result};
 use block::ConcreteBlock;
@@ -590,18 +590,18 @@ impl platform::Platform for MacPlatform {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn platform::Window> {
-        Box::new(Window::open(id, options, executor, self.fonts()))
+        Box::new(MacWindow::open(handle, options, executor, self.fonts()))
     }
 
-    fn main_window_id(&self) -> Option<usize> {
-        Window::main_window_id()
+    fn main_window(&self) -> Option<AnyWindowHandle> {
+        MacWindow::main_window()
     }
 
-    fn add_status_item(&self, _id: usize) -> Box<dyn platform::Window> {
+    fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
         Box::new(StatusItem::add(self.fonts()))
     }
 

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

@@ -13,6 +13,7 @@ use crate::{
         Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton,
         MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind,
     },
+    AnyWindowHandle,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -282,7 +283,7 @@ struct InsertText {
 }
 
 struct WindowState {
-    id: usize,
+    handle: AnyWindowHandle,
     native_window: id,
     kind: WindowKind,
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
@@ -422,11 +423,11 @@ impl WindowState {
     }
 }
 
-pub struct Window(Rc<RefCell<WindowState>>);
+pub struct MacWindow(Rc<RefCell<WindowState>>);
 
-impl Window {
+impl MacWindow {
     pub fn open(
-        id: usize,
+        handle: AnyWindowHandle,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
         fonts: Arc<dyn platform::FontSystem>,
@@ -504,7 +505,7 @@ impl Window {
             assert!(!native_view.is_null());
 
             let window = Self(Rc::new(RefCell::new(WindowState {
-                id,
+                handle,
                 native_window,
                 kind: options.kind,
                 event_callback: None,
@@ -621,13 +622,13 @@ impl Window {
         }
     }
 
-    pub fn main_window_id() -> Option<usize> {
+    pub fn main_window() -> Option<AnyWindowHandle> {
         unsafe {
             let app = NSApplication::sharedApplication(nil);
             let main_window: id = msg_send![app, mainWindow];
             if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
-                let id = get_window_state(&*main_window).borrow().id;
-                Some(id)
+                let handle = get_window_state(&*main_window).borrow().handle;
+                Some(handle)
             } else {
                 None
             }
@@ -635,7 +636,7 @@ impl Window {
     }
 }
 
-impl Drop for Window {
+impl Drop for MacWindow {
     fn drop(&mut self) {
         let this = self.0.borrow();
         let window = this.native_window;
@@ -649,7 +650,7 @@ impl Drop for Window {
     }
 }
 
-impl platform::Window for Window {
+impl platform::Window for MacWindow {
     fn bounds(&self) -> WindowBounds {
         self.0.as_ref().borrow().bounds()
     }
@@ -881,7 +882,7 @@ impl platform::Window for Window {
 
     fn is_topmost_for_position(&self, position: Vector2F) -> bool {
         let self_borrow = self.0.borrow();
-        let self_id = self_borrow.id;
+        let self_handle = self_borrow.handle;
 
         unsafe {
             let app = NSApplication::sharedApplication(nil);
@@ -898,8 +899,8 @@ impl platform::Window for Window {
             let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
             let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
             if is_panel == YES || is_window == YES {
-                let topmost_window_id = get_window_state(&*top_most_window).borrow().id;
-                topmost_window_id == self_id
+                let topmost_window = get_window_state(&*top_most_window).borrow().handle;
+                topmost_window == self_handle
             } else {
                 // Someone else's window is on top
                 false

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

@@ -5,7 +5,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     keymap_matcher::KeymapMatcher,
-    Action, ClipboardItem, Menu,
+    Action, AnyWindowHandle, ClipboardItem, Menu,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -102,7 +102,7 @@ pub struct Platform {
     fonts: Arc<dyn super::FontSystem>,
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
     cursor: Mutex<CursorStyle>,
-    active_window_id: Arc<Mutex<Option<usize>>>,
+    active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
 }
 
 impl Platform {
@@ -112,7 +112,7 @@ impl Platform {
             fonts: Arc::new(super::current::FontSystem::new()),
             current_clipboard_item: Default::default(),
             cursor: Mutex::new(CursorStyle::Arrow),
-            active_window_id: Default::default(),
+            active_window: Default::default(),
         }
     }
 }
@@ -146,30 +146,30 @@ impl super::Platform for Platform {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: super::WindowOptions,
         _executor: Rc<super::executor::Foreground>,
     ) -> Box<dyn super::Window> {
-        *self.active_window_id.lock() = Some(id);
+        *self.active_window.lock() = Some(handle);
         Box::new(Window::new(
-            id,
+            handle,
             match options.bounds {
                 WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
                 WindowBounds::Fixed(rect) => rect.size(),
             },
-            self.active_window_id.clone(),
+            self.active_window.clone(),
         ))
     }
 
-    fn main_window_id(&self) -> Option<usize> {
-        self.active_window_id.lock().clone()
+    fn main_window(&self) -> Option<AnyWindowHandle> {
+        self.active_window.lock().clone()
     }
 
-    fn add_status_item(&self, id: usize) -> Box<dyn crate::platform::Window> {
+    fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn crate::platform::Window> {
         Box::new(Window::new(
-            id,
+            handle,
             vec2f(24., 24.),
-            self.active_window_id.clone(),
+            self.active_window.clone(),
         ))
     }
 
@@ -256,7 +256,7 @@ impl super::Screen for Screen {
 }
 
 pub struct Window {
-    id: usize,
+    handle: AnyWindowHandle,
     pub(crate) size: Vector2F,
     scale_factor: f32,
     current_scene: Option<crate::Scene>,
@@ -270,13 +270,17 @@ pub struct Window {
     pub(crate) title: Option<String>,
     pub(crate) edited: bool,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
-    active_window_id: Arc<Mutex<Option<usize>>>,
+    active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
 }
 
 impl Window {
-    pub fn new(id: usize, size: Vector2F, active_window_id: Arc<Mutex<Option<usize>>>) -> Self {
+    pub fn new(
+        handle: AnyWindowHandle,
+        size: Vector2F,
+        active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    ) -> Self {
         Self {
-            id,
+            handle,
             size,
             event_handlers: Default::default(),
             resize_handlers: Default::default(),
@@ -290,7 +294,7 @@ impl Window {
             title: None,
             edited: false,
             pending_prompts: Default::default(),
-            active_window_id,
+            active_window,
         }
     }
 
@@ -342,7 +346,7 @@ impl super::Window for Window {
     }
 
     fn activate(&self) {
-        *self.active_window_id.lock() = Some(self.id);
+        *self.active_window.lock() = Some(self.handle);
     }
 
     fn set_title(&mut self, title: &str) {

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

@@ -32,6 +32,7 @@ pub struct MouseDrag {
     pub region: RectF,
     pub prev_mouse_position: Vector2F,
     pub platform_event: MouseMovedEvent,
+    pub end: bool,
 }
 
 impl Deref for MouseDrag {

crates/language/src/language.rs 🔗

@@ -182,8 +182,8 @@ impl CachedLspAdapter {
         self.adapter.workspace_configuration(cx)
     }
 
-    pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
-        self.adapter.process_diagnostics(params).await
+    pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+        self.adapter.process_diagnostics(params)
     }
 
     pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
@@ -262,7 +262,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary>;
 
-    async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
 
@@ -339,6 +339,8 @@ pub struct LanguageConfig {
     #[serde(default)]
     pub line_comment: Option<Arc<str>>,
     #[serde(default)]
+    pub collapsed_placeholder: String,
+    #[serde(default)]
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
     #[serde(default)]
     pub overrides: HashMap<String, LanguageConfigOverride>,
@@ -408,6 +410,7 @@ impl Default for LanguageConfig {
             line_comment: Default::default(),
             block_comment: Default::default(),
             overrides: Default::default(),
+            collapsed_placeholder: Default::default(),
         }
     }
 }
@@ -523,9 +526,10 @@ pub struct OutlineConfig {
 pub struct EmbeddingConfig {
     pub query: Query,
     pub item_capture_ix: u32,
-    pub name_capture_ix: u32,
+    pub name_capture_ix: Option<u32>,
     pub context_capture_ix: Option<u32>,
-    pub extra_context_capture_ix: Option<u32>,
+    pub collapse_capture_ix: Option<u32>,
+    pub keep_capture_ix: Option<u32>,
 }
 
 struct InjectionConfig {
@@ -840,8 +844,8 @@ impl LanguageRegistry {
                                             }
                                         }
                                     }
-                                    Err(err) => {
-                                        log::error!("failed to load language {name} - {err}");
+                                    Err(e) => {
+                                        log::error!("failed to load language {name}:\n{:?}", e);
                                         let mut state = this.state.write();
                                         state.mark_language_loaded(id);
                                         if let Some(mut txs) = state.loading_languages.remove(&id) {
@@ -849,7 +853,7 @@ impl LanguageRegistry {
                                                 let _ = tx.send(Err(anyhow!(
                                                     "failed to load language {}: {}",
                                                     name,
-                                                    err
+                                                    e
                                                 )));
                                             }
                                         }
@@ -1184,25 +1188,39 @@ impl Language {
 
     pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
         if let Some(query) = queries.highlights {
-            self = self.with_highlights_query(query.as_ref())?;
+            self = self
+                .with_highlights_query(query.as_ref())
+                .context("Error loading highlights query")?;
         }
         if let Some(query) = queries.brackets {
-            self = self.with_brackets_query(query.as_ref())?;
+            self = self
+                .with_brackets_query(query.as_ref())
+                .context("Error loading brackets query")?;
         }
         if let Some(query) = queries.indents {
-            self = self.with_indents_query(query.as_ref())?;
+            self = self
+                .with_indents_query(query.as_ref())
+                .context("Error loading indents query")?;
         }
         if let Some(query) = queries.outline {
-            self = self.with_outline_query(query.as_ref())?;
+            self = self
+                .with_outline_query(query.as_ref())
+                .context("Error loading outline query")?;
         }
         if let Some(query) = queries.embedding {
-            self = self.with_embedding_query(query.as_ref())?;
+            self = self
+                .with_embedding_query(query.as_ref())
+                .context("Error loading embedding query")?;
         }
         if let Some(query) = queries.injections {
-            self = self.with_injection_query(query.as_ref())?;
+            self = self
+                .with_injection_query(query.as_ref())
+                .context("Error loading injection query")?;
         }
         if let Some(query) = queries.overrides {
-            self = self.with_override_query(query.as_ref())?;
+            self = self
+                .with_override_query(query.as_ref())
+                .context("Error loading override query")?;
         }
         Ok(self)
     }
@@ -1247,23 +1265,26 @@ impl Language {
         let mut item_capture_ix = None;
         let mut name_capture_ix = None;
         let mut context_capture_ix = None;
-        let mut extra_context_capture_ix = None;
+        let mut collapse_capture_ix = None;
+        let mut keep_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
                 ("item", &mut item_capture_ix),
                 ("name", &mut name_capture_ix),
                 ("context", &mut context_capture_ix),
-                ("context.extra", &mut extra_context_capture_ix),
+                ("keep", &mut keep_capture_ix),
+                ("collapse", &mut collapse_capture_ix),
             ],
         );
-        if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
+        if let Some(item_capture_ix) = item_capture_ix {
             grammar.embedding_config = Some(EmbeddingConfig {
                 query,
                 item_capture_ix,
                 name_capture_ix,
                 context_capture_ix,
-                extra_context_capture_ix,
+                collapse_capture_ix,
+                keep_capture_ix,
             });
         }
         Ok(self)
@@ -1466,12 +1487,6 @@ impl Language {
         None
     }
 
-    pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
-        for adapter in &self.adapters {
-            adapter.process_diagnostics(diagnostics).await;
-        }
-    }
-
     pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
         for adapter in &self.adapters {
             adapter.process_completion(completion).await;
@@ -1548,9 +1563,20 @@ impl Language {
     pub fn grammar(&self) -> Option<&Arc<Grammar>> {
         self.grammar.as_ref()
     }
+
+    pub fn default_scope(self: &Arc<Self>) -> LanguageScope {
+        LanguageScope {
+            language: self.clone(),
+            override_id: None,
+        }
+    }
 }
 
 impl LanguageScope {
+    pub fn collapsed_placeholder(&self) -> &str {
+        self.language.config.collapsed_placeholder.as_ref()
+    }
+
     pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
         Override::as_option(
             self.config_override().map(|o| &o.line_comment),
@@ -1724,7 +1750,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         unreachable!();
     }
 
-    async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
         self.disk_based_diagnostics_sources.clone()

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -61,7 +61,9 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
         .receive_notification::<lsp::notification::DidOpenTextDocument>()
         .await;
 
-    let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
+    let log_view = cx
+        .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx))
+        .root(cx);
 
     language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
         message: "hello from the server".into(),

crates/live_kit_client/build.rs 🔗

@@ -58,11 +58,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
         "cargo:rerun-if-changed={}/Package.resolved",
         SWIFT_PACKAGE_NAME
     );
+
     let swift_package_root = swift_package_root();
+    let swift_target_folder = swift_target_folder();
     if !Command::new("swift")
         .arg("build")
         .args(["--configuration", &env::var("PROFILE").unwrap()])
         .args(["--triple", &swift_target.target.triple])
+        .args(["--build-path".into(), swift_target_folder])
         .current_dir(&swift_package_root)
         .status()
         .unwrap()
@@ -128,6 +131,12 @@ fn swift_package_root() -> PathBuf {
     env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
 }
 
+fn swift_target_folder() -> PathBuf {
+    env::current_dir()
+        .unwrap()
+        .join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
+}
+
 fn copy_dir(source: &Path, destination: &Path) {
     assert!(
         Command::new("rm")
@@ -155,8 +164,7 @@ fn copy_dir(source: &Path, destination: &Path) {
 
 impl SwiftTarget {
     fn out_dir_path(&self) -> PathBuf {
-        swift_package_root()
-            .join(".build")
+        swift_target_folder()
             .join(&self.target.unversioned_triple)
             .join(env::var("PROFILE").unwrap())
     }

crates/node_runtime/src/node_runtime.rs 🔗

@@ -1,9 +1,6 @@
 use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
-use futures::lock::Mutex;
-use futures::{future::Shared, FutureExt};
-use gpui::{executor::Background, Task};
 use serde::Deserialize;
 use smol::{fs, io::BufReader, process::Command};
 use std::process::{Output, Stdio};
@@ -33,20 +30,12 @@ pub struct NpmInfoDistTags {
 
 pub struct NodeRuntime {
     http: Arc<dyn HttpClient>,
-    background: Arc<Background>,
-    installation_path: Mutex<Option<Shared<Task<Result<PathBuf, Arc<anyhow::Error>>>>>>,
 }
 
 impl NodeRuntime {
-    pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+    pub fn instance(http: Arc<dyn HttpClient>) -> Arc<NodeRuntime> {
         RUNTIME_INSTANCE
-            .get_or_init(|| {
-                Arc::new(NodeRuntime {
-                    http,
-                    background,
-                    installation_path: Mutex::new(None),
-                })
-            })
+            .get_or_init(|| Arc::new(NodeRuntime { http }))
             .clone()
     }
 
@@ -61,7 +50,9 @@ impl NodeRuntime {
         subcommand: &str,
         args: &[&str],
     ) -> Result<Output> {
-        let attempt = |installation_path: PathBuf| async move {
+        let attempt = || async move {
+            let installation_path = self.install_if_needed().await?;
+
             let mut env_path = installation_path.join("bin").into_os_string();
             if let Some(existing_path) = std::env::var_os("PATH") {
                 if !existing_path.is_empty() {
@@ -92,10 +83,9 @@ impl NodeRuntime {
             command.output().await.map_err(|e| anyhow!("{e}"))
         };
 
-        let installation_path = self.install_if_needed().await?;
-        let mut output = attempt(installation_path.clone()).await;
+        let mut output = attempt().await;
         if output.is_err() {
-            output = attempt(installation_path).await;
+            output = attempt().await;
             if output.is_err() {
                 return Err(anyhow!(
                     "failed to launch npm subcommand {subcommand} subcommand"
@@ -167,23 +157,8 @@ impl NodeRuntime {
     }
 
     async fn install_if_needed(&self) -> Result<PathBuf> {
-        let task = self
-            .installation_path
-            .lock()
-            .await
-            .get_or_insert_with(|| {
-                let http = self.http.clone();
-                self.background
-                    .spawn(async move { Self::install(http).await.map_err(Arc::new) })
-                    .shared()
-            })
-            .clone();
-
-        task.await.map_err(|e| anyhow!("{}", e))
-    }
+        log::info!("Node runtime install_if_needed");
 
-    async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
-        log::info!("installing Node runtime");
         let arch = match consts::ARCH {
             "x86_64" => "x64",
             "aarch64" => "arm64",
@@ -214,7 +189,8 @@ impl NodeRuntime {
 
             let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
             let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
-            let mut response = http
+            let mut response = self
+                .http
                 .get(&url, Default::default(), true)
                 .await
                 .context("error downloading Node binary tarball")?;

crates/project/src/project.rs 🔗

@@ -2769,24 +2769,21 @@ impl Project {
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
-                move |mut params, cx| {
+                move |mut params, mut cx| {
                     let this = this;
                     let adapter = adapter.clone();
-                    cx.spawn(|mut cx| async move {
-                        adapter.process_diagnostics(&mut params).await;
-                        if let Some(this) = this.upgrade(&cx) {
-                            this.update(&mut cx, |this, cx| {
-                                this.update_diagnostics(
-                                    server_id,
-                                    params,
-                                    &adapter.disk_based_diagnostic_sources,
-                                    cx,
-                                )
-                                .log_err();
-                            });
-                        }
-                    })
-                    .detach();
+                    adapter.process_diagnostics(&mut params);
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.update_diagnostics(
+                                server_id,
+                                params,
+                                &adapter.disk_based_diagnostic_sources,
+                                cx,
+                            )
+                            .log_err();
+                        });
+                    }
                 }
             })
             .detach();

crates/project/src/project_tests.rs 🔗

@@ -1,7 +1,6 @@
-use crate::{worktree::WorktreeHandle, Event, *};
+use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *};
 use fs::{FakeFs, LineEnding, RealFs};
 use futures::{future, StreamExt};
-use globset::Glob;
 use gpui::{executor::Deterministic, test::subscribe, AppContext};
 use language::{
     language_settings::{AllLanguageSettings, LanguageSettingsContent},
@@ -3641,7 +3640,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 search_query,
                 false,
                 true,
-                vec![Glob::new("*.odd").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.odd").unwrap()],
                 Vec::new()
             ),
             cx
@@ -3659,7 +3658,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 search_query,
                 false,
                 true,
-                vec![Glob::new("*.rs").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.rs").unwrap()],
                 Vec::new()
             ),
             cx
@@ -3681,8 +3680,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 vec![
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher(),
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap(),
                 ],
                 Vec::new()
             ),
@@ -3705,9 +3704,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 vec![
-                    Glob::new("*.rs").unwrap().compile_matcher(),
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher(),
+                    PathMatcher::new("*.rs").unwrap(),
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap(),
                 ],
                 Vec::new()
             ),
@@ -3752,7 +3751,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 Vec::new(),
-                vec![Glob::new("*.odd").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.odd").unwrap()],
             ),
             cx
         )
@@ -3775,7 +3774,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 false,
                 true,
                 Vec::new(),
-                vec![Glob::new("*.rs").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.rs").unwrap()],
             ),
             cx
         )
@@ -3797,8 +3796,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 Vec::new(),
                 vec![
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher(),
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap(),
                 ],
             ),
             cx
@@ -3821,9 +3820,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 Vec::new(),
                 vec![
-                    Glob::new("*.rs").unwrap().compile_matcher(),
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher(),
+                    PathMatcher::new("*.rs").unwrap(),
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap(),
                 ],
             ),
             cx
@@ -3860,8 +3859,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 search_query,
                 false,
                 true,
-                vec![Glob::new("*.odd").unwrap().compile_matcher()],
-                vec![Glob::new("*.odd").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.odd").unwrap()],
+                vec![PathMatcher::new("*.odd").unwrap()],
             ),
             cx
         )
@@ -3878,8 +3877,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 search_query,
                 false,
                 true,
-                vec![Glob::new("*.ts").unwrap().compile_matcher()],
-                vec![Glob::new("*.ts").unwrap().compile_matcher()],
+                vec![PathMatcher::new("*.ts").unwrap()],
+                vec![PathMatcher::new("*.ts").unwrap()],
             ),
             cx
         )
@@ -3897,12 +3896,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 vec![
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher()
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap()
                 ],
                 vec![
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher()
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap()
                 ],
             ),
             cx
@@ -3921,12 +3920,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 false,
                 true,
                 vec![
-                    Glob::new("*.ts").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher()
+                    PathMatcher::new("*.ts").unwrap(),
+                    PathMatcher::new("*.odd").unwrap()
                 ],
                 vec![
-                    Glob::new("*.rs").unwrap().compile_matcher(),
-                    Glob::new("*.odd").unwrap().compile_matcher()
+                    PathMatcher::new("*.rs").unwrap(),
+                    PathMatcher::new("*.odd").unwrap()
                 ],
             ),
             cx

crates/project/src/search.rs 🔗

@@ -1,5 +1,5 @@
 use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
-use anyhow::Result;
+use anyhow::{Context, Result};
 use client::proto;
 use globset::{Glob, GlobMatcher};
 use itertools::Itertools;
@@ -9,7 +9,7 @@ use smol::future::yield_now;
 use std::{
     io::{BufRead, BufReader, Read},
     ops::Range,
-    path::Path,
+    path::{Path, PathBuf},
     sync::Arc,
 };
 
@@ -20,8 +20,8 @@ pub enum SearchQuery {
         query: Arc<str>,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<GlobMatcher>,
-        files_to_exclude: Vec<GlobMatcher>,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
     },
     Regex {
         regex: Regex,
@@ -29,18 +29,43 @@ pub enum SearchQuery {
         multiline: bool,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<GlobMatcher>,
-        files_to_exclude: Vec<GlobMatcher>,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
     },
 }
 
+#[derive(Clone, Debug)]
+pub struct PathMatcher {
+    maybe_path: PathBuf,
+    glob: GlobMatcher,
+}
+
+impl std::fmt::Display for PathMatcher {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.maybe_path.to_string_lossy().fmt(f)
+    }
+}
+
+impl PathMatcher {
+    pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
+        Ok(PathMatcher {
+            glob: Glob::new(&maybe_glob)?.compile_matcher(),
+            maybe_path: PathBuf::from(maybe_glob),
+        })
+    }
+
+    pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
+        other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other)
+    }
+}
+
 impl SearchQuery {
     pub fn text(
         query: impl ToString,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<GlobMatcher>,
-        files_to_exclude: Vec<GlobMatcher>,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
     ) -> Self {
         let query = query.to_string();
         let search = AhoCorasickBuilder::new()
@@ -61,8 +86,8 @@ impl SearchQuery {
         query: impl ToString,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<GlobMatcher>,
-        files_to_exclude: Vec<GlobMatcher>,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
     ) -> Result<Self> {
         let mut query = query.to_string();
         let initial_query = Arc::from(query.as_str());
@@ -96,16 +121,16 @@ impl SearchQuery {
                 message.query,
                 message.whole_word,
                 message.case_sensitive,
-                deserialize_globs(&message.files_to_include)?,
-                deserialize_globs(&message.files_to_exclude)?,
+                deserialize_path_matches(&message.files_to_include)?,
+                deserialize_path_matches(&message.files_to_exclude)?,
             )
         } else {
             Ok(Self::text(
                 message.query,
                 message.whole_word,
                 message.case_sensitive,
-                deserialize_globs(&message.files_to_include)?,
-                deserialize_globs(&message.files_to_exclude)?,
+                deserialize_path_matches(&message.files_to_include)?,
+                deserialize_path_matches(&message.files_to_exclude)?,
             ))
         }
     }
@@ -120,12 +145,12 @@ impl SearchQuery {
             files_to_include: self
                 .files_to_include()
                 .iter()
-                .map(|g| g.glob().to_string())
+                .map(|matcher| matcher.to_string())
                 .join(","),
             files_to_exclude: self
                 .files_to_exclude()
                 .iter()
-                .map(|g| g.glob().to_string())
+                .map(|matcher| matcher.to_string())
                 .join(","),
         }
     }
@@ -266,7 +291,7 @@ impl SearchQuery {
         matches!(self, Self::Regex { .. })
     }
 
-    pub fn files_to_include(&self) -> &[GlobMatcher] {
+    pub fn files_to_include(&self) -> &[PathMatcher] {
         match self {
             Self::Text {
                 files_to_include, ..
@@ -277,7 +302,7 @@ impl SearchQuery {
         }
     }
 
-    pub fn files_to_exclude(&self) -> &[GlobMatcher] {
+    pub fn files_to_exclude(&self) -> &[PathMatcher] {
         match self {
             Self::Text {
                 files_to_exclude, ..
@@ -306,11 +331,63 @@ impl SearchQuery {
     }
 }
 
-fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
+fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<Vec<PathMatcher>> {
     glob_set
         .split(',')
         .map(str::trim)
         .filter(|glob_str| !glob_str.is_empty())
-        .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
+        .map(|glob_str| {
+            PathMatcher::new(glob_str)
+                .with_context(|| format!("deserializing path match glob {glob_str}"))
+        })
         .collect()
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn path_matcher_creation_for_valid_paths() {
+        for valid_path in [
+            "file",
+            "Cargo.toml",
+            ".DS_Store",
+            "~/dir/another_dir/",
+            "./dir/file",
+            "dir/[a-z].txt",
+            "../dir/filé",
+        ] {
+            let path_matcher = PathMatcher::new(valid_path).unwrap_or_else(|e| {
+                panic!("Valid path {valid_path} should be accepted, but got: {e}")
+            });
+            assert!(
+                path_matcher.is_match(valid_path),
+                "Path matcher for valid path {valid_path} should match itself"
+            )
+        }
+    }
+
+    #[test]
+    fn path_matcher_creation_for_globs() {
+        for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
+            match PathMatcher::new(invalid_glob) {
+                Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
+                Err(_expected) => {}
+            }
+        }
+
+        for valid_glob in [
+            "dir/?ile",
+            "dir/*.txt",
+            "dir/**/file",
+            "dir/[a-z].txt",
+            "{dir,file}",
+        ] {
+            match PathMatcher::new(valid_glob) {
+                Ok(_expected) => {}
+                Err(e) => panic!("Valid glob {valid_glob} should be accepted, but got: {e}"),
+            }
+        }
+    }
+}

crates/project/src/terminals.rs 🔗

@@ -1,5 +1,5 @@
 use crate::Project;
-use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
 use std::path::PathBuf;
 use terminal::{Terminal, TerminalBuilder, TerminalSettings};
 
@@ -11,7 +11,7 @@ impl Project {
     pub fn create_terminal(
         &mut self,
         working_directory: Option<PathBuf>,
-        window_id: usize,
+        window: AnyWindowHandle,
         cx: &mut ModelContext<Self>,
     ) -> anyhow::Result<ModelHandle<Terminal>> {
         if self.is_remote() {
@@ -27,7 +27,7 @@ impl Project {
                 settings.env.clone(),
                 Some(settings.blinking.clone()),
                 settings.alternate_scroll,
-                window_id,
+                window,
             )
             .map(|builder| {
                 let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));

crates/project/src/worktree.rs 🔗

@@ -2369,7 +2369,7 @@ impl BackgroundScannerState {
         }
 
         // Remove any git repositories whose .git entry no longer exists.
-        let mut snapshot = &mut self.snapshot;
+        let snapshot = &mut self.snapshot;
         let mut repositories = mem::take(&mut snapshot.git_repositories);
         let mut repository_entries = mem::take(&mut snapshot.repository_entries);
         repositories.retain(|work_directory_id, _| {

crates/project_panel/src/file_associations.rs 🔗

@@ -4,7 +4,7 @@ use collections::HashMap;
 
 use gpui::{AppContext, AssetSource};
 use serde_derive::Deserialize;
-use util::iife;
+use util::{iife, paths::PathExt};
 
 #[derive(Deserialize, Debug)]
 struct TypeConfig {
@@ -48,14 +48,7 @@ impl FileAssociations {
             // FIXME: Associate a type with the languages and have the file's langauge
             //        override these associations
             iife!({
-                let suffix = path
-                    .file_name()
-                    .and_then(|os_str| os_str.to_str())
-                    .and_then(|file_name| {
-                        file_name
-                            .find('.')
-                            .and_then(|dot_index| file_name.get(dot_index + 1..))
-                    })?;
+                let suffix = path.icon_suffix()?;
 
                 this.suffixes
                     .get(suffix)

crates/project_panel/src/project_panel.rs 🔗

@@ -115,6 +115,7 @@ actions!(
     [
         ExpandSelectedEntry,
         CollapseSelectedEntry,
+        CollapseAllEntries,
         NewDirectory,
         NewFile,
         Copy,
@@ -140,6 +141,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     file_associations::init(assets, cx);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
+    cx.add_action(ProjectPanel::collapse_all_entries);
     cx.add_action(ProjectPanel::select_prev);
     cx.add_action(ProjectPanel::select_next);
     cx.add_action(ProjectPanel::new_file);
@@ -430,7 +432,7 @@ impl ProjectPanel {
             menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
             if entry.is_dir() {
                 menu_entries.push(ContextMenuItem::action(
-                    "Search inside",
+                    "Search Inside",
                     NewSearchInDirectory,
                 ));
             }
@@ -514,6 +516,12 @@ impl ProjectPanel {
         }
     }
 
+    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
+        self.expanded_dir_ids.clear();
+        self.update_visible_entries(None, cx);
+        cx.notify();
+    }
+
     fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
         if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
             if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
@@ -1407,7 +1415,7 @@ impl ProjectPanel {
 
             if cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
                 .is_some()
                 && dragged_entry_destination
                     .as_ref()
@@ -1451,7 +1459,7 @@ impl ProjectPanel {
         .on_up(MouseButton::Left, move |_, this, cx| {
             if let Some((_, dragged_entry)) = cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
             {
                 this.move_entry(
                     *dragged_entry,
@@ -1464,7 +1472,7 @@ impl ProjectPanel {
         .on_move(move |_, this, cx| {
             if cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
                 .is_some()
             {
                 this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
@@ -1718,7 +1726,7 @@ impl ClipboardEntry {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use gpui::{TestAppContext, ViewHandle};
+    use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
     use pretty_assertions::assert_eq;
     use project::FakeFs;
     use serde_json::json;
@@ -1772,7 +1780,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1860,7 +1870,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -1882,7 +1893,7 @@ mod tests {
         // Add a file with the root folder selected. The filename editor is placed
         // before the first file in the root folder.
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2211,7 +2222,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -2233,7 +2245,7 @@ mod tests {
         // Add a file with the root folder selected. The filename editor is placed
         // before the first file in the root folder.
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2311,7 +2323,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         panel.update(cx, |panel, cx| {
@@ -2384,7 +2398,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         toggle_expand_dir(&panel, "src/test", cx);
@@ -2401,9 +2416,9 @@ mod tests {
                 "          third.rs"
             ]
         );
-        ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
+        ensure_single_file_is_opened(window, "test/first.rs", cx);
 
-        submit_deletion(window_id, &panel, cx);
+        submit_deletion(window.into(), &panel, cx);
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
@@ -2414,7 +2429,7 @@ mod tests {
             ],
             "Project panel should have no deleted file, no other file is selected in it"
         );
-        ensure_no_open_items_and_panes(window_id, &workspace, cx);
+        ensure_no_open_items_and_panes(window.into(), &workspace, cx);
 
         select_path(&panel, "src/test/second.rs", cx);
         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
@@ -2428,9 +2443,9 @@ mod tests {
                 "          third.rs"
             ]
         );
-        ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
+        ensure_single_file_is_opened(window, "test/second.rs", cx);
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             let active_items = workspace
                 .read(cx)
                 .panes()
@@ -2446,13 +2461,13 @@ mod tests {
                 .expect("Open item should be an editor");
             open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
         });
-        submit_deletion(window_id, &panel, cx);
+        submit_deletion(window.into(), &panel, cx);
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &["v src", "    v test", "          third.rs"],
             "Project panel should have no deleted file, with one last file remaining"
         );
-        ensure_no_open_items_and_panes(window_id, &workspace, cx);
+        ensure_no_open_items_and_panes(window.into(), &workspace, cx);
     }
 
     #[gpui::test]
@@ -2473,7 +2488,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "src/", cx);
@@ -2484,7 +2500,7 @@ mod tests {
             &["v src  <== selected", "    > test"]
         );
         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2515,7 +2531,7 @@ mod tests {
             &["v src", "    > test  <== selected"]
         );
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2565,7 +2581,7 @@ mod tests {
             ],
         );
         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2619,7 +2635,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         let new_search_events_count = Arc::new(AtomicUsize::new(0));
@@ -2678,6 +2696,65 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/project_root",
+            json!({
+                "dir_1": {
+                    "nested_dir": {
+                        "file_a.py": "# File contents",
+                        "file_b.py": "# File contents",
+                        "file_c.py": "# File contents",
+                    },
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                },
+                "dir_2": {
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        panel.update(cx, |panel, cx| {
+            panel.collapse_all_entries(&CollapseAllEntries, cx)
+        });
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v project_root", "    > dir_1", "    > dir_2",]
+        );
+
+        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
+        toggle_expand_dir(&panel, "project_root/dir_1", cx);
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v project_root",
+                "    v dir_1  <== selected",
+                "        > nested_dir",
+                "          file_1.py",
+                "          file_2.py",
+                "          file_3.py",
+                "    > dir_2",
+            ]
+        );
+    }
+
     fn toggle_expand_dir(
         panel: &ViewHandle<ProjectPanel>,
         path: impl AsRef<Path>,
@@ -2801,13 +2878,11 @@ mod tests {
     }
 
     fn ensure_single_file_is_opened(
-        window_id: usize,
-        workspace: &ViewHandle<Workspace>,
+        window: WindowHandle<Workspace>,
         expected_path: &str,
         cx: &mut TestAppContext,
     ) {
-        cx.read_window(window_id, |cx| {
-            let workspace = workspace.read(cx);
+        window.update_root(cx, |workspace, cx| {
             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
             let worktree_id = WorktreeId::from_usize(worktrees[0].id());
@@ -2829,12 +2904,12 @@ mod tests {
     }
 
     fn submit_deletion(
-        window_id: usize,
+        window: AnyWindowHandle,
         panel: &ViewHandle<ProjectPanel>,
         cx: &mut TestAppContext,
     ) {
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts before the deletion"
         );
         panel.update(cx, |panel, cx| {
@@ -2844,27 +2919,27 @@ mod tests {
                 .detach_and_log_err(cx);
         });
         assert!(
-            cx.has_pending_prompt(window_id),
+            window.has_pending_prompt(cx),
             "Should have a prompt after the deletion"
         );
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts after prompt was replied to"
         );
         cx.foreground().run_until_parked();
     }
 
     fn ensure_no_open_items_and_panes(
-        window_id: usize,
+        window: AnyWindowHandle,
         workspace: &ViewHandle<Workspace>,
         cx: &mut TestAppContext,
     ) {
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts after deletion operation closes the file"
         );
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let open_project_paths = workspace
                 .read(cx)
                 .panes()
@@ -2878,3 +2953,4 @@ mod tests {
         });
     }
 }
+// TODO - a workspace command?

crates/project_symbols/src/project_symbols.rs 🔗

@@ -326,10 +326,11 @@ mod tests {
             },
         );
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Create the project symbols view.
-        let symbols = cx.add_view(window_id, |cx| {
+        let symbols = window.add_view(cx, |cx| {
             ProjectSymbols::new(
                 ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
                 cx,

crates/recent_projects/src/highlighted_workspace_location.rs 🔗

@@ -5,6 +5,7 @@ use gpui::{
     elements::{Label, LabelStyle},
     AnyElement, Element, View,
 };
+use util::paths::PathExt;
 use workspace::WorkspaceLocation;
 
 pub struct HighlightedText {
@@ -61,7 +62,7 @@ impl HighlightedWorkspaceLocation {
             .paths()
             .iter()
             .map(|path| {
-                let path = util::paths::compact(&path);
+                let path = path.compact();
                 let highlighted_text = Self::highlights_for_path(
                     path.as_ref(),
                     &string_match.positions,

crates/recent_projects/src/recent_projects.rs 🔗

@@ -11,6 +11,7 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::sync::Arc;
+use util::paths::PathExt;
 use workspace::{
     notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
     WORKSPACE_DB,
@@ -134,7 +135,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 let combined_string = location
                     .paths()
                     .iter()
-                    .map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
+                    .map(|path| path.compact().to_string_lossy().into_owned())
                     .collect::<Vec<_>>()
                     .join("");
                 StringMatchCandidate::new(id, combined_string)

crates/search/Cargo.toml 🔗

@@ -20,6 +20,7 @@ settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
+semantic_index = { path = "../semantic_index" }
 anyhow.workspace = true
 futures.workspace = true
 log.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
-    ToggleRegex, ToggleWholeWord,
+    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
@@ -46,6 +46,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::select_prev_match_on_pane);
     cx.add_action(BufferSearchBar::select_all_matches_on_pane);
     cx.add_action(BufferSearchBar::handle_editor_cancel);
+    cx.add_action(BufferSearchBar::next_history_query);
+    cx.add_action(BufferSearchBar::previous_history_query);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
     add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
@@ -65,7 +67,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
 }
 
 pub struct BufferSearchBar {
-    pub query_editor: ViewHandle<Editor>,
+    query_editor: ViewHandle<Editor>,
     active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
     active_match_index: Option<usize>,
     active_searchable_item_subscription: Option<Subscription>,
@@ -76,6 +78,7 @@ pub struct BufferSearchBar {
     default_options: SearchOptions,
     query_contains_error: bool,
     dismissed: bool,
+    search_history: SearchHistory,
 }
 
 impl Entity for BufferSearchBar {
@@ -106,6 +109,48 @@ impl View for BufferSearchBar {
             .map(|active_searchable_item| active_searchable_item.supported_options())
             .unwrap_or_default();
 
+        let previous_query_keystrokes =
+            cx.binding_for_action(&PreviousHistoryQuery {})
+                .map(|binding| {
+                    binding
+                        .keystrokes()
+                        .iter()
+                        .map(|k| k.to_string())
+                        .collect::<Vec<_>>()
+                });
+        let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+            binding
+                .keystrokes()
+                .iter()
+                .map(|k| k.to_string())
+                .collect::<Vec<_>>()
+        });
+        let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+            (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+                format!(
+                    "Search ({}/{} for previous/next query)",
+                    previous_query_keystrokes.join(" "),
+                    next_query_keystrokes.join(" ")
+                )
+            }
+            (None, Some(next_query_keystrokes)) => {
+                format!(
+                    "Search ({} for next query)",
+                    next_query_keystrokes.join(" ")
+                )
+            }
+            (Some(previous_query_keystrokes), None) => {
+                format!(
+                    "Search ({} for previous query)",
+                    previous_query_keystrokes.join(" ")
+                )
+            }
+            (None, None) => String::new(),
+        };
+        self.query_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text(new_placeholder_text, cx);
+        });
+
         Flex::row()
             .with_child(
                 Flex::row()
@@ -258,6 +303,7 @@ impl BufferSearchBar {
             pending_search: None,
             query_contains_error: false,
             dismissed: true,
+            search_history: SearchHistory::default(),
         }
     }
 
@@ -341,7 +387,7 @@ impl BufferSearchBar {
         cx: &mut ViewContext<Self>,
     ) -> oneshot::Receiver<()> {
         let options = options.unwrap_or(self.default_options);
-        if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
+        if query != self.query(cx) || self.search_options != options {
             self.query_editor.update(cx, |query_editor, cx| {
                 query_editor.buffer().update(cx, |query_buffer, cx| {
                     let len = query_buffer.len(cx);
@@ -674,7 +720,7 @@ impl BufferSearchBar {
 
     fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
         let (done_tx, done_rx) = oneshot::channel();
-        let query = self.query_editor.read(cx).text(cx);
+        let query = self.query(cx);
         self.pending_search.take();
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
@@ -707,6 +753,7 @@ impl BufferSearchBar {
                     )
                 };
 
+                let query_text = query.as_str().to_string();
                 let matches = active_searchable_item.find_matches(query, cx);
 
                 let active_searchable_item = active_searchable_item.downgrade();
@@ -720,6 +767,7 @@ impl BufferSearchBar {
                                 .insert(active_searchable_item.downgrade(), matches);
 
                             this.update_match_index(cx);
+                            this.search_history.add(query_text);
                             if !this.dismissed {
                                 let matches = this
                                     .searchable_items_with_matches
@@ -753,6 +801,28 @@ impl BufferSearchBar {
             cx.notify();
         }
     }
+
+    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(new_query) = self.search_history.next().map(str::to_string) {
+            let _ = self.search(&new_query, Some(self.search_options), cx);
+        } else {
+            self.search_history.reset_selection();
+            let _ = self.search("", Some(self.search_options), cx);
+        }
+    }
+
+    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+        if self.query(cx).is_empty() {
+            if let Some(new_query) = self.search_history.current().map(str::to_string) {
+                let _ = self.search(&new_query, Some(self.search_options), cx);
+                return;
+            }
+        }
+
+        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
+            let _ = self.search(&new_query, Some(self.search_options), cx);
+        }
+    }
 }
 
 #[cfg(test)]
@@ -779,11 +849,10 @@ mod tests {
                 cx,
             )
         });
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
-
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let window = cx.add_window(|_| EmptyView);
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = window.add_view(cx, |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);
@@ -1159,11 +1228,10 @@ mod tests {
             "Should pick a query with multiple results"
         );
         let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
-
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let window = cx.add_window(|_| EmptyView);
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = window.add_view(cx, |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);
@@ -1179,12 +1247,13 @@ mod tests {
             search_bar.activate_current_match(cx);
         });
 
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 !editor.is_focused(cx),
                 "Initially, the editor should not be focused"
             );
         });
+
         let initial_selections = editor.update(cx, |editor, cx| {
             let initial_selections = editor.selections.display_ranges(cx);
             assert_eq!(
@@ -1201,7 +1270,7 @@ mod tests {
             cx.focus(search_bar.query_editor.as_any());
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should focus editor after successful SelectAllMatches"
@@ -1225,7 +1294,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_next_match(&SelectNextMatch, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should still have editor focused after SelectNextMatch"
@@ -1254,7 +1323,7 @@ mod tests {
             cx.focus(search_bar.query_editor.as_any());
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should focus editor after successful SelectAllMatches"
@@ -1278,7 +1347,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_prev_match(&SelectPrevMatch, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should still have editor focused after SelectPrevMatch"
@@ -1314,7 +1383,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 !editor.is_focused(cx),
                 "Should not switch focus to editor if SelectAllMatches does not find any matches"
@@ -1333,4 +1402,154 @@ mod tests {
             );
         });
     }
+
+    #[gpui::test]
+    async fn test_search_query_history(cx: &mut TestAppContext) {
+        crate::project_search::tests::init_test(cx);
+
+        let buffer_text = r#"
+        A regular expression (shortened as regex or regexp;[1] also referred to as
+        rational expression[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+        .unindent();
+        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
+        let window = cx.add_window(|_| EmptyView);
+
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+        let search_bar = window.add_view(cx, |cx| {
+            let mut search_bar = BufferSearchBar::new(cx);
+            search_bar.set_active_pane_item(Some(&editor), cx);
+            search_bar.show(cx);
+            search_bar
+        });
+
+        // Add 3 search items into the history.
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+            .await
+            .unwrap();
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
+            .await
+            .unwrap();
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
+            })
+            .await
+            .unwrap();
+        // Ensure that the latest search is active.
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next history query after the latest should set the query to the empty string.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // First previous query for empty current query should set the query to the latest.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Further previous items should go over the history in reverse order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Previous items should never go behind the first history item.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "a");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "a");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next items should go over the history in the original order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
+            .await
+            .unwrap();
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "ba");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+
+        // New search input should add another entry to history and move the selection to the end of the history.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "ba");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+    }
 }

crates/search/src/project_search.rs 🔗

@@ -1,15 +1,14 @@
 use crate::{
-    SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
-    ToggleWholeWord,
+    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch,
+    SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
 };
-use anyhow::Result;
+use anyhow::Context;
 use collections::HashMap;
 use editor::{
     items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
     SelectAll, MAX_TAB_TITLE_LEN,
 };
 use futures::StreamExt;
-use globset::{Glob, GlobMatcher};
 use gpui::{
     actions,
     elements::*,
@@ -18,7 +17,12 @@ use gpui::{
     Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use menu::Confirm;
-use project::{search::SearchQuery, Entry, Project};
+use postage::stream::Stream;
+use project::{
+    search::{PathMatcher, SearchQuery},
+    Entry, Project,
+};
+use semantic_index::SemanticIndex;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -36,7 +40,10 @@ use workspace::{
     ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
 };
 
-actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
+actions!(
+    project_search,
+    [SearchInNew, ToggleFocus, NextField, ToggleSemanticSearch]
+);
 
 #[derive(Default)]
 struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
@@ -49,6 +56,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
+    cx.add_action(ProjectSearchBar::next_history_query);
+    cx.add_action(ProjectSearchBar::previous_history_query);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
@@ -76,6 +85,7 @@ struct ProjectSearch {
     match_ranges: Vec<Range<Anchor>>,
     active_query: Option<SearchQuery>,
     search_id: usize,
+    search_history: SearchHistory,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -89,6 +99,7 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
+    semantic: Option<SemanticSearchState>,
     search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
@@ -98,6 +109,12 @@ pub struct ProjectSearchView {
     excluded_files_editor: ViewHandle<Editor>,
 }
 
+struct SemanticSearchState {
+    file_count: usize,
+    outstanding_file_count: usize,
+    _progress_task: Task<()>,
+}
+
 pub struct ProjectSearchBar {
     active_project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -117,6 +134,7 @@ impl ProjectSearch {
             match_ranges: Default::default(),
             active_query: None,
             search_id: 0,
+            search_history: SearchHistory::default(),
         }
     }
 
@@ -130,6 +148,7 @@ impl ProjectSearch {
             match_ranges: self.match_ranges.clone(),
             active_query: self.active_query.clone(),
             search_id: self.search_id,
+            search_history: self.search_history.clone(),
         })
     }
 
@@ -138,6 +157,7 @@ impl ProjectSearch {
             .project
             .update(cx, |project, cx| project.search(query.clone(), cx));
         self.search_id += 1;
+        self.search_history.add(query.as_str().to_string());
         self.active_query = Some(query);
         self.match_ranges.clear();
         self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
@@ -172,6 +192,58 @@ impl ProjectSearch {
         }));
         cx.notify();
     }
+
+    fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
+        let search = SemanticIndex::global(cx).map(|index| {
+            index.update(cx, |semantic_index, cx| {
+                semantic_index.search_project(
+                    self.project.clone(),
+                    query.as_str().to_owned(),
+                    10,
+                    query.files_to_include().to_vec(),
+                    query.files_to_exclude().to_vec(),
+                    cx,
+                )
+            })
+        });
+        self.search_id += 1;
+        self.match_ranges.clear();
+        self.search_history.add(query.as_str().to_string());
+        self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+            let results = search?.await.log_err()?;
+
+            let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
+                this.excerpts.update(cx, |excerpts, cx| {
+                    excerpts.clear(cx);
+
+                    let matches = results
+                        .into_iter()
+                        .map(|result| (result.buffer, vec![result.range.start..result.range.start]))
+                        .collect();
+
+                    excerpts.stream_excerpts_with_context_lines(matches, 3, cx)
+                })
+            });
+
+            while let Some(match_range) = match_ranges.next().await {
+                this.update(&mut cx, |this, cx| {
+                    this.match_ranges.push(match_range);
+                    while let Ok(Some(match_range)) = match_ranges.try_next() {
+                        this.match_ranges.push(match_range);
+                    }
+                    cx.notify();
+                });
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.pending_search.take();
+                cx.notify();
+            });
+
+            None
+        }));
+        cx.notify();
+    }
 }
 
 pub enum ViewEvent {
@@ -195,13 +267,67 @@ impl View for ProjectSearchView {
             enum Status {}
 
             let theme = theme::current(cx).clone();
-            let text = if self.query_editor.read(cx).text(cx).is_empty() {
-                ""
-            } else if model.pending_search.is_some() {
-                "Searching..."
+            let text = if model.pending_search.is_some() {
+                Cow::Borrowed("Searching...")
+            } else if let Some(semantic) = &self.semantic {
+                if semantic.outstanding_file_count > 0 {
+                    Cow::Owned(format!(
+                        "Indexing. {} of {}...",
+                        semantic.file_count - semantic.outstanding_file_count,
+                        semantic.file_count
+                    ))
+                } else {
+                    Cow::Borrowed("Indexing complete")
+                }
+            } else if self.query_editor.read(cx).text(cx).is_empty() {
+                Cow::Borrowed("")
             } else {
-                "No results"
+                Cow::Borrowed("No results")
             };
+
+            let previous_query_keystrokes =
+                cx.binding_for_action(&PreviousHistoryQuery {})
+                    .map(|binding| {
+                        binding
+                            .keystrokes()
+                            .iter()
+                            .map(|k| k.to_string())
+                            .collect::<Vec<_>>()
+                    });
+            let next_query_keystrokes =
+                cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+                    binding
+                        .keystrokes()
+                        .iter()
+                        .map(|k| k.to_string())
+                        .collect::<Vec<_>>()
+                });
+            let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+                (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+                    format!(
+                        "Search ({}/{} for previous/next query)",
+                        previous_query_keystrokes.join(" "),
+                        next_query_keystrokes.join(" ")
+                    )
+                }
+                (None, Some(next_query_keystrokes)) => {
+                    format!(
+                        "Search ({} for next query)",
+                        next_query_keystrokes.join(" ")
+                    )
+                }
+                (Some(previous_query_keystrokes), None) => {
+                    format!(
+                        "Search ({} for previous query)",
+                        previous_query_keystrokes.join(" ")
+                    )
+                }
+                (None, None) => String::new(),
+            };
+            self.query_editor.update(cx, |editor, cx| {
+                editor.set_placeholder_text(new_placeholder_text, cx);
+            });
+
             MouseEventHandler::<Status, _>::new(0, cx, |_, _| {
                 Label::new(text, theme.search.results_status.clone())
                     .aligned()
@@ -490,6 +616,7 @@ impl ProjectSearchView {
             model,
             query_editor,
             results_editor,
+            semantic: None,
             search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
@@ -509,8 +636,7 @@ impl ProjectSearchView {
         if !dir_entry.is_dir() {
             return;
         }
-        let filter_path = dir_entry.path.join("**");
-        let Some(filter_str) = filter_path.to_str() else { return; };
+        let Some(filter_str) = dir_entry.path.to_str() else { return; };
 
         let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
         let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
@@ -577,6 +703,16 @@ impl ProjectSearchView {
     }
 
     fn search(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(semantic) = &mut self.semantic {
+            if semantic.outstanding_file_count > 0 {
+                return;
+            }
+            if let Some(query) = self.build_search_query(cx) {
+                self.model
+                    .update(cx, |model, cx| model.semantic_search(query, cx));
+            }
+        }
+
         if let Some(query) = self.build_search_query(cx) {
             self.model.update(cx, |model, cx| model.search(query, cx));
         }
@@ -585,7 +721,7 @@ impl ProjectSearchView {
     fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
         let text = self.query_editor.read(cx).text(cx);
         let included_files =
-            match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) {
+            match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
                 Ok(included_files) => {
                     self.panels_with_errors.remove(&InputPanel::Include);
                     included_files
@@ -597,7 +733,7 @@ impl ProjectSearchView {
                 }
             };
         let excluded_files =
-            match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) {
+            match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
                 Ok(excluded_files) => {
                     self.panels_with_errors.remove(&InputPanel::Exclude);
                     excluded_files
@@ -637,11 +773,14 @@ impl ProjectSearchView {
         }
     }
 
-    fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
+    fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
         text.split(',')
             .map(str::trim)
-            .filter(|glob_str| !glob_str.is_empty())
-            .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
+            .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
+            .map(|maybe_glob_str| {
+                PathMatcher::new(maybe_glob_str)
+                    .with_context(|| format!("parsing {maybe_glob_str} as path matcher"))
+            })
             .collect()
     }
 
@@ -654,6 +793,7 @@ impl ProjectSearchView {
 
             let range_to_select = match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
+                let range_to_select = editor.range_for_match(&range_to_select);
                 editor.unfold_ranges([range_to_select.clone()], false, true, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.select_ranges([range_to_select])
@@ -695,8 +835,12 @@ impl ProjectSearchView {
             let is_new_search = self.search_id != prev_search_id;
             self.results_editor.update(cx, |editor, cx| {
                 if is_new_search {
+                    let range_to_select = match_ranges
+                        .first()
+                        .clone()
+                        .map(|range| editor.range_for_match(range));
                     editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                        s.select_ranges(match_ranges.first().cloned())
+                        s.select_ranges(range_to_select)
                     });
                 }
                 editor.highlight_background::<Self>(
@@ -873,6 +1017,7 @@ impl ProjectSearchBar {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
                 search_view.search_options.toggle(option);
+                search_view.semantic = None;
                 search_view.search(cx);
             });
             cx.notify();
@@ -882,6 +1027,61 @@ impl ProjectSearchBar {
         }
     }
 
+    fn toggle_semantic_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                if search_view.semantic.is_some() {
+                    search_view.semantic = None;
+                } else if let Some(semantic_index) = SemanticIndex::global(cx) {
+                    // TODO: confirm that it's ok to send this project
+                    search_view.search_options = SearchOptions::none();
+
+                    let project = search_view.model.read(cx).project.clone();
+                    let index_task = semantic_index.update(cx, |semantic_index, cx| {
+                        semantic_index.index_project(project, cx)
+                    });
+
+                    cx.spawn(|search_view, mut cx| async move {
+                        let (files_to_index, mut files_remaining_rx) = index_task.await?;
+
+                        search_view.update(&mut cx, |search_view, cx| {
+                            cx.notify();
+                            search_view.semantic = Some(SemanticSearchState {
+                                file_count: files_to_index,
+                                outstanding_file_count: files_to_index,
+                                _progress_task: cx.spawn(|search_view, mut cx| async move {
+                                    while let Some(count) = files_remaining_rx.recv().await {
+                                        search_view
+                                            .update(&mut cx, |search_view, cx| {
+                                                if let Some(semantic_search_state) =
+                                                    &mut search_view.semantic
+                                                {
+                                                    semantic_search_state.outstanding_file_count =
+                                                        count;
+                                                    cx.notify();
+                                                    if count == 0 {
+                                                        return;
+                                                    }
+                                                }
+                                            })
+                                            .ok();
+                                    }
+                                }),
+                            });
+                        })?;
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+                cx.notify();
+            });
+            cx.notify();
+            true
+        } else {
+            false
+        }
+    }
+
     fn render_nav_button(
         &self,
         icon: &'static str,
@@ -959,6 +1159,42 @@ impl ProjectSearchBar {
         .into_any()
     }
 
+    fn render_semantic_search_button(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        let is_active = if let Some(search) = self.active_project_search.as_ref() {
+            let search = search.read(cx);
+            search.semantic.is_some()
+        } else {
+            false
+        };
+
+        let region_id = 3;
+
+        MouseEventHandler::<Self, _>::new(region_id, cx, |state, cx| {
+            let theme = theme::current(cx);
+            let style = theme
+                .search
+                .option_button
+                .in_state(is_active)
+                .style_for(state);
+            Label::new("Semantic", style.text.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.toggle_semantic_search(cx);
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<Self>(
+            region_id,
+            format!("Toggle Semantic Search"),
+            Some(Box::new(ToggleSemanticSearch)),
+            tooltip_style,
+            cx,
+        )
+        .into_any()
+    }
+
     fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
         if let Some(search) = self.active_project_search.as_ref() {
             search.read(cx).search_options.contains(option)
@@ -966,6 +1202,47 @@ impl ProjectSearchBar {
             false
         }
     }
+
+    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                let new_query = search_view.model.update(cx, |model, _| {
+                    if let Some(new_query) = model.search_history.next().map(str::to_string) {
+                        new_query
+                    } else {
+                        model.search_history.reset_selection();
+                        String::new()
+                    }
+                });
+                search_view.set_query(&new_query, cx);
+            });
+        }
+    }
+
+    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                if search_view.query_editor.read(cx).text(cx).is_empty() {
+                    if let Some(new_query) = search_view
+                        .model
+                        .read(cx)
+                        .search_history
+                        .current()
+                        .map(str::to_string)
+                    {
+                        search_view.set_query(&new_query, cx);
+                        return;
+                    }
+                }
+
+                if let Some(new_query) = search_view.model.update(cx, |model, _| {
+                    model.search_history.previous().map(str::to_string)
+                }) {
+                    search_view.set_query(&new_query, cx);
+                }
+            });
+        }
+    }
 }
 
 impl Entity for ProjectSearchBar {
@@ -1048,8 +1325,14 @@ impl View for ProjectSearchBar {
                                 .with_child(self.render_nav_button(">", Direction::Next, cx))
                                 .aligned(),
                         )
-                        .with_child(
-                            Flex::row()
+                        .with_child({
+                            let row = if SemanticIndex::enabled(cx) {
+                                Flex::row().with_child(self.render_semantic_search_button(cx))
+                            } else {
+                                Flex::row()
+                            };
+
+                            let row = row
                                 .with_child(self.render_option_button(
                                     "Case",
                                     SearchOptions::CASE_SENSITIVE,
@@ -1067,8 +1350,10 @@ impl View for ProjectSearchBar {
                                 ))
                                 .contained()
                                 .with_style(theme.search.option_button_group)
-                                .aligned(),
-                        )
+                                .aligned();
+
+                            row
+                        })
                         .contained()
                         .with_margin_bottom(row_spacing),
                 )
@@ -1139,6 +1424,7 @@ pub mod tests {
     use editor::DisplayPoint;
     use gpui::{color::Color, executor::Deterministic, TestAppContext};
     use project::FakeFs;
+    use semantic_index::semantic_index_settings::SemanticIndexSettings;
     use serde_json::json;
     use settings::SettingsStore;
     use std::sync::Arc;
@@ -1161,7 +1447,9 @@ pub mod tests {
         .await;
         let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
-        let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx));
+        let search_view = cx
+            .add_window(|cx| ProjectSearchView::new(search.clone(), cx))
+            .root(cx);
 
         search_view.update(cx, |search_view, cx| {
             search_view
@@ -1278,7 +1566,8 @@ pub mod tests {
         )
         .await;
         let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let active_item = cx.read(|cx| {
             workspace
@@ -1309,9 +1598,9 @@ pub mod tests {
         };
         let search_view_id = search_view.id();
 
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1362,7 +1651,7 @@ pub mod tests {
             );
         });
         cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
+            |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
         )
         .detach();
         deterministic.run_until_parked();
@@ -1393,9 +1682,9 @@ pub mod tests {
                 "Search view with mismatching query should be focused after search results are available",
             );
         });
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1423,9 +1712,9 @@ pub mod tests {
             );
         });
 
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1462,7 +1751,9 @@ pub mod tests {
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
 
         let active_item = cx.read(|cx| {
             workspace
@@ -1540,7 +1831,7 @@ pub mod tests {
             search_view.included_files_editor.update(cx, |editor, cx| {
                 assert_eq!(
                     editor.display_text(cx),
-                    a_dir_entry.path.join("**").display().to_string(),
+                    a_dir_entry.path.to_str().unwrap(),
                     "New search in directory should have included dir entry path"
                 );
             });
@@ -1564,6 +1855,193 @@ pub mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_search_query_history(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "one.rs": "const ONE: usize = 1;",
+                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        workspace.update(cx, |workspace, cx| {
+            ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+        });
+
+        let search_view = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .active_pane()
+                .read(cx)
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+                .expect("Search view expected to appear after new search event trigger")
+        });
+
+        let search_bar = window.add_view(cx, |cx| {
+            let mut search_bar = ProjectSearchBar::new();
+            search_bar.set_active_pane_item(Some(&search_view), cx);
+            // search_bar.show(cx);
+            search_bar
+        });
+
+        // Add 3 search items into the history + another unsubmitted one.
+        search_view.update(cx, |search_view, cx| {
+            search_view.search_options = SearchOptions::CASE_SENSITIVE;
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view.query_editor.update(cx, |query_editor, cx| {
+                query_editor.set_text("JUST_TEXT_INPUT", cx)
+            });
+        });
+        cx.foreground().run_until_parked();
+
+        // Ensure that the latest input with search settings is active.
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(
+                search_view.query_editor.read(cx).text(cx),
+                "JUST_TEXT_INPUT"
+            );
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next history query after the latest should set the query to the empty string.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // First previous query for empty current query should set the query to the latest submitted one.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Further previous items should go over the history in reverse order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Previous items should never go behind the first history item.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next items should go over the history in the original order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // New search input should add another entry to history and move the selection to the end of the history.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+    }
+
     pub fn init_test(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
         let fonts = cx.font_cache();
@@ -1573,6 +2051,7 @@ pub mod tests {
         cx.update(|cx| {
             cx.set_global(SettingsStore::test(cx));
             cx.set_global(ActiveSearches::default());
+            settings::register::<SemanticIndexSettings>(cx);
 
             theme::init((), cx);
             cx.update_global::<SettingsStore, _, _>(|store, _| {

crates/search/src/search.rs 🔗

@@ -3,6 +3,7 @@ pub use buffer_search::BufferSearchBar;
 use gpui::{actions, Action, AppContext};
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
+use smallvec::SmallVec;
 
 pub mod buffer_search;
 pub mod project_search;
@@ -21,6 +22,8 @@ actions!(
         SelectNextMatch,
         SelectPrevMatch,
         SelectAllMatches,
+        NextHistoryQuery,
+        PreviousHistoryQuery,
     ]
 );
 
@@ -53,6 +56,10 @@ impl SearchOptions {
         }
     }
 
+    pub fn none() -> SearchOptions {
+        SearchOptions::NONE
+    }
+
     pub fn from_query(query: &SearchQuery) -> SearchOptions {
         let mut options = SearchOptions::NONE;
         options.set(SearchOptions::WHOLE_WORD, query.whole_word());
@@ -61,3 +68,187 @@ impl SearchOptions {
         options
     }
 }
+
+const SEARCH_HISTORY_LIMIT: usize = 20;
+
+#[derive(Default, Debug, Clone)]
+pub struct SearchHistory {
+    history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
+    selected: Option<usize>,
+}
+
+impl SearchHistory {
+    pub fn add(&mut self, search_string: String) {
+        if let Some(i) = self.selected {
+            if search_string == self.history[i] {
+                return;
+            }
+        }
+
+        if let Some(previously_searched) = self.history.last_mut() {
+            if search_string.find(previously_searched.as_str()).is_some() {
+                *previously_searched = search_string;
+                self.selected = Some(self.history.len() - 1);
+                return;
+            }
+        }
+
+        self.history.push(search_string);
+        if self.history.len() > SEARCH_HISTORY_LIMIT {
+            self.history.remove(0);
+        }
+        self.selected = Some(self.history.len() - 1);
+    }
+
+    pub fn next(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let selected = self.selected?;
+        if selected == history_size - 1 {
+            return None;
+        }
+        let next_index = selected + 1;
+        self.selected = Some(next_index);
+        Some(&self.history[next_index])
+    }
+
+    pub fn current(&self) -> Option<&str> {
+        Some(&self.history[self.selected?])
+    }
+
+    pub fn previous(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let prev_index = match self.selected {
+            Some(selected_index) => {
+                if selected_index == 0 {
+                    return None;
+                } else {
+                    selected_index - 1
+                }
+            }
+            None => history_size - 1,
+        };
+
+        self.selected = Some(prev_index);
+        Some(&self.history[prev_index])
+    }
+
+    pub fn reset_selection(&mut self) {
+        self.selected = None;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_add() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.current(),
+            None,
+            "No current selection should be set fo the default search history"
+        );
+
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.current(),
+            Some("rust"),
+            "Newly added item should be selected"
+        );
+
+        // check if duplicates are not added
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should not add a duplicate"
+        );
+        assert_eq!(search_history.current(), Some("rust"));
+
+        // check if new string containing the previous string replaces it
+        search_history.add("rustlang".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should replace previous item if it's a substring"
+        );
+        assert_eq!(search_history.current(), Some("rustlang"));
+
+        // push enough items to test SEARCH_HISTORY_LIMIT
+        for i in 0..SEARCH_HISTORY_LIMIT * 2 {
+            search_history.add(format!("item{i}"));
+        }
+        assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
+    }
+
+    #[test]
+    fn test_next_and_previous() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.next(),
+            None,
+            "Default search history should not have a next item"
+        );
+
+        search_history.add("Rust".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("JavaScript".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("TypeScript".to_string());
+        assert_eq!(search_history.next(), None);
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.previous(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.previous(), Some("Rust"));
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.previous(), None);
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.next(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.next(), Some("TypeScript"));
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.next(), None);
+        assert_eq!(search_history.current(), Some("TypeScript"));
+    }
+
+    #[test]
+    fn test_reset_selection() {
+        let mut search_history = SearchHistory::default();
+        search_history.add("Rust".to_string());
+        search_history.add("JavaScript".to_string());
+        search_history.add("TypeScript".to_string());
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+        assert_eq!(
+            search_history.previous(),
+            Some("TypeScript"),
+            "Should start from the end after reset on previous item query"
+        );
+
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("JavaScript"));
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+    }
+}

crates/vector_store/Cargo.toml → crates/semantic_index/Cargo.toml 🔗

@@ -1,11 +1,11 @@
 [package]
-name = "vector_store"
+name = "semantic_index"
 version = "0.1.0"
 edition = "2021"
 publish = false
 
 [lib]
-path = "src/vector_store.rs"
+path = "src/semantic_index.rs"
 doctest = false
 
 [dependencies]
@@ -20,6 +20,7 @@ editor = { path = "../editor" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 anyhow.workspace = true
+postage.workspace = true
 futures.workspace = true
 smol.workspace = true
 rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
@@ -33,8 +34,10 @@ 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
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
@@ -43,7 +46,20 @@ project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"]}
-tree-sitter-rust = "*"
+
+pretty_assertions.workspace = true
 rand.workspace = true
 unindent.workspace = true
 tempdir.workspace = true
+ctor.workspace = true
+env_logger.workspace = true
+
+tree-sitter-typescript.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-lua.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-php.workspace = true

crates/vector_store/src/db.rs → crates/semantic_index/src/db.rs 🔗

@@ -1,21 +1,21 @@
+use crate::{parsing::Document, SEMANTIC_INDEX_VERSION};
+use anyhow::{anyhow, Context, Result};
+use project::{search::PathMatcher, Fs};
+use rpc::proto::Timestamp;
+use rusqlite::{
+    params,
+    types::{FromSql, FromSqlResult, ValueRef},
+};
 use std::{
     cmp::Ordering,
     collections::HashMap,
+    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
+    sync::Arc,
     time::SystemTime,
 };
 
-use anyhow::{anyhow, Result};
-
-use crate::parsing::ParsedFile;
-use crate::VECTOR_STORE_VERSION;
-use rpc::proto::Timestamp;
-use rusqlite::{
-    params,
-    types::{FromSql, FromSqlResult, ValueRef},
-};
-
 #[derive(Debug)]
 pub struct FileRecord {
     pub id: usize,
@@ -42,48 +42,94 @@ pub struct VectorDatabase {
 }
 
 impl VectorDatabase {
-    pub fn new(path: String) -> Result<Self> {
+    pub async fn new(fs: Arc<dyn Fs>, path: Arc<PathBuf>) -> Result<Self> {
+        if let Some(db_directory) = path.parent() {
+            fs.create_dir(db_directory).await?;
+        }
+
         let this = Self {
-            db: rusqlite::Connection::open(path)?,
+            db: rusqlite::Connection::open(path.as_path())?,
         };
         this.initialize_database()?;
         Ok(this)
     }
 
+    fn get_existing_version(&self) -> Result<i64> {
+        let mut version_query = self
+            .db
+            .prepare("SELECT version from semantic_index_config")?;
+        version_query
+            .query_row([], |row| Ok(row.get::<_, i64>(0)?))
+            .map_err(|err| anyhow!("version query failed: {err}"))
+    }
+
     fn initialize_database(&self) -> Result<()> {
         rusqlite::vtab::array::load_module(&self.db)?;
 
-        // This will create the database if it doesnt exist
+        // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
+        if self
+            .get_existing_version()
+            .map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64)
+        {
+            log::trace!("vector database schema up to date");
+            return Ok(());
+        }
+
+        log::trace!("vector database schema out of date. updating...");
+        self.db
+            .execute("DROP TABLE IF EXISTS documents", [])
+            .context("failed to drop 'documents' table")?;
+        self.db
+            .execute("DROP TABLE IF EXISTS files", [])
+            .context("failed to drop 'files' table")?;
+        self.db
+            .execute("DROP TABLE IF EXISTS worktrees", [])
+            .context("failed to drop 'worktrees' table")?;
+        self.db
+            .execute("DROP TABLE IF EXISTS semantic_index_config", [])
+            .context("failed to drop 'semantic_index_config' table")?;
 
         // Initialize Vector Databasing Tables
         self.db.execute(
-            "CREATE TABLE IF NOT EXISTS worktrees (
+            "CREATE TABLE semantic_index_config (
+                version INTEGER NOT NULL
+            )",
+            [],
+        )?;
+
+        self.db.execute(
+            "INSERT INTO semantic_index_config (version) VALUES (?1)",
+            params![SEMANTIC_INDEX_VERSION],
+        )?;
+
+        self.db.execute(
+            "CREATE TABLE worktrees (
                 id INTEGER PRIMARY KEY AUTOINCREMENT,
                 absolute_path VARCHAR NOT NULL
             );
-            CREATE UNIQUE INDEX IF NOT EXISTS worktrees_absolute_path ON worktrees (absolute_path);
+            CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
             ",
             [],
         )?;
 
         self.db.execute(
-            "CREATE TABLE IF NOT EXISTS files (
+            "CREATE TABLE files (
                 id INTEGER PRIMARY KEY AUTOINCREMENT,
                 worktree_id INTEGER NOT NULL,
                 relative_path VARCHAR NOT NULL,
                 mtime_seconds INTEGER NOT NULL,
                 mtime_nanos INTEGER NOT NULL,
-                vector_store_version INTEGER NOT NULL,
                 FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
             )",
             [],
         )?;
 
         self.db.execute(
-            "CREATE TABLE IF NOT EXISTS documents (
+            "CREATE TABLE documents (
                 id INTEGER PRIMARY KEY AUTOINCREMENT,
                 file_id INTEGER NOT NULL,
-                offset INTEGER NOT NULL,
+                start_byte INTEGER NOT NULL,
+                end_byte INTEGER NOT NULL,
                 name VARCHAR NOT NULL,
                 embedding BLOB NOT NULL,
                 FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
@@ -91,6 +137,7 @@ impl VectorDatabase {
             [],
         )?;
 
+        log::trace!("vector database initialized with updated schema.");
         Ok(())
     }
 
@@ -102,43 +149,44 @@ impl VectorDatabase {
         Ok(())
     }
 
-    pub fn insert_file(&self, worktree_id: i64, indexed_file: ParsedFile) -> Result<()> {
+    pub fn insert_file(
+        &self,
+        worktree_id: i64,
+        path: PathBuf,
+        mtime: SystemTime,
+        documents: Vec<Document>,
+    ) -> Result<()> {
         // Write to files table, and return generated id.
         self.db.execute(
             "
             DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;
             ",
-            params![worktree_id, indexed_file.path.to_str()],
+            params![worktree_id, path.to_str()],
         )?;
-        let mtime = Timestamp::from(indexed_file.mtime);
+        let mtime = Timestamp::from(mtime);
         self.db.execute(
             "
             INSERT INTO files
-            (worktree_id, relative_path, mtime_seconds, mtime_nanos, vector_store_version)
+            (worktree_id, relative_path, mtime_seconds, mtime_nanos)
             VALUES
-            (?1, ?2, $3, $4, $5);
+            (?1, ?2, $3, $4);
             ",
-            params![
-                worktree_id,
-                indexed_file.path.to_str(),
-                mtime.seconds,
-                mtime.nanos,
-                VECTOR_STORE_VERSION
-            ],
+            params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
         )?;
 
         let file_id = self.db.last_insert_rowid();
 
         // Currently inserting at approximately 3400 documents a second
         // I imagine we can speed this up with a bulk insert of some kind.
-        for document in indexed_file.documents {
+        for document in documents {
             let embedding_blob = bincode::serialize(&document.embedding)?;
 
             self.db.execute(
-                "INSERT INTO documents (file_id, offset, name, embedding) VALUES (?1, ?2, ?3, ?4)",
+                "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)",
                 params![
                     file_id,
-                    document.offset.to_string(),
+                    document.range.start.to_string(),
+                    document.range.end.to_string(),
                     document.name,
                     embedding_blob
                 ],
@@ -148,6 +196,23 @@ impl VectorDatabase {
         Ok(())
     }
 
+    pub fn worktree_previously_indexed(&self, worktree_root_path: &Path) -> Result<bool> {
+        let mut worktree_query = self
+            .db
+            .prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+        let worktree_id = worktree_query
+            .query_row(params![worktree_root_path.to_string_lossy()], |row| {
+                Ok(row.get::<_, i64>(0)?)
+            })
+            .map_err(|err| anyhow!(err));
+
+        if worktree_id.is_ok() {
+            return Ok(true);
+        } else {
+            return Ok(false);
+        }
+    }
+
     pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result<i64> {
         // Check that the absolute path doesnt exist
         let mut worktree_query = self
@@ -201,12 +266,12 @@ impl VectorDatabase {
 
     pub fn top_k_search(
         &self,
-        worktree_ids: &[i64],
         query_embedding: &Vec<f32>,
         limit: usize,
-    ) -> Result<Vec<(i64, PathBuf, usize, String)>> {
+        file_ids: &[i64],
+    ) -> Result<Vec<(i64, f32)>> {
         let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
-        self.for_each_document(&worktree_ids, |id, embedding| {
+        self.for_each_document(file_ids, |id, embedding| {
             let similarity = dot(&embedding, &query_embedding);
             let ix = match results
                 .binary_search_by(|(_, s)| similarity.partial_cmp(&s).unwrap_or(Ordering::Equal))
@@ -218,29 +283,57 @@ impl VectorDatabase {
             results.truncate(limit);
         })?;
 
-        let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<_>>();
-        self.get_documents_by_ids(&ids)
+        Ok(results)
     }
 
-    fn for_each_document(
+    pub fn retrieve_included_file_ids(
         &self,
         worktree_ids: &[i64],
-        mut f: impl FnMut(i64, Vec<f32>),
-    ) -> Result<()> {
+        includes: &[PathMatcher],
+        excludes: &[PathMatcher],
+    ) -> Result<Vec<i64>> {
+        let mut file_query = self.db.prepare(
+            "
+            SELECT
+                id, relative_path
+            FROM
+                files
+            WHERE
+                worktree_id IN rarray(?)
+            ",
+        )?;
+
+        let mut file_ids = Vec::<i64>::new();
+        let mut rows = file_query.query([ids_to_sql(worktree_ids)])?;
+
+        while let Some(row) = rows.next()? {
+            let file_id = row.get(0)?;
+            let relative_path = row.get_ref(1)?.as_str()?;
+            let included =
+                includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
+            let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
+            if included && !excluded {
+                file_ids.push(file_id);
+            }
+        }
+
+        Ok(file_ids)
+    }
+
+    fn for_each_document(&self, file_ids: &[i64], mut f: impl FnMut(i64, Vec<f32>)) -> Result<()> {
         let mut query_statement = self.db.prepare(
             "
             SELECT
-                documents.id, documents.embedding
+                id, embedding
             FROM
-                documents, files
+                documents
             WHERE
-                documents.file_id = files.id AND
-                files.worktree_id IN rarray(?)
+                file_id IN rarray(?)
             ",
         )?;
 
         query_statement
-            .query_map(params![ids_to_sql(worktree_ids)], |row| {
+            .query_map(params![ids_to_sql(&file_ids)], |row| {
                 Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
             })?
             .filter_map(|row| row.ok())
@@ -248,11 +341,15 @@ impl VectorDatabase {
         Ok(())
     }
 
-    fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, usize, String)>> {
+    pub fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, Range<usize>)>> {
         let mut statement = self.db.prepare(
             "
                 SELECT
-                    documents.id, files.worktree_id, files.relative_path, documents.offset, documents.name
+                    documents.id,
+                    files.worktree_id,
+                    files.relative_path,
+                    documents.start_byte,
+                    documents.end_byte
                 FROM
                     documents, files
                 WHERE
@@ -266,15 +363,14 @@ impl VectorDatabase {
                 row.get::<_, i64>(0)?,
                 row.get::<_, i64>(1)?,
                 row.get::<_, String>(2)?.into(),
-                row.get(3)?,
-                row.get(4)?,
+                row.get(3)?..row.get(4)?,
             ))
         })?;
 
-        let mut values_by_id = HashMap::<i64, (i64, PathBuf, usize, String)>::default();
+        let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
         for row in result_iter {
-            let (id, worktree_id, path, offset, name) = row?;
-            values_by_id.insert(id, (worktree_id, path, offset, name));
+            let (id, worktree_id, path, range) = row?;
+            values_by_id.insert(id, (worktree_id, path, range));
         }
 
         let mut results = Vec::with_capacity(ids.len());

crates/vector_store/src/embedding.rs → crates/semantic_index/src/embedding.rs 🔗

@@ -67,17 +67,16 @@ impl EmbeddingProvider for DummyEmbeddings {
     }
 }
 
-const INPUT_LIMIT: usize = 8190;
+const OPENAI_INPUT_LIMIT: usize = 8190;
 
 impl OpenAIEmbeddings {
     fn truncate(span: String) -> String {
         let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref());
-        if tokens.len() > INPUT_LIMIT {
-            tokens.truncate(INPUT_LIMIT);
+        if tokens.len() > OPENAI_INPUT_LIMIT {
+            tokens.truncate(OPENAI_INPUT_LIMIT);
             let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
             if result.is_ok() {
                 let transformed = result.unwrap();
-                // assert_ne!(transformed, span);
                 return transformed;
             }
         }
@@ -88,6 +87,7 @@ impl OpenAIEmbeddings {
     async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> {
         let request = Request::post("https://api.openai.com/v1/embeddings")
             .redirect_policy(isahc::config::RedirectPolicy::Follow)
+            .timeout(Duration::from_secs(4))
             .header("Content-Type", "application/json")
             .header("Authorization", format!("Bearer {}", api_key))
             .body(
@@ -106,7 +106,7 @@ impl OpenAIEmbeddings {
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
     async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
-        const BACKOFF_SECONDS: [usize; 3] = [65, 180, 360];
+        const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125];
         const MAX_RETRIES: usize = 3;
 
         let api_key = OPENAI_API_KEY
@@ -114,6 +114,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
             .ok_or_else(|| anyhow!("no api key"))?;
 
         let mut request_number = 0;
+        let mut truncated = false;
         let mut response: Response<AsyncBody>;
         let mut spans: Vec<String> = spans.iter().map(|x| x.to_string()).collect();
         while request_number < MAX_RETRIES {
@@ -132,14 +133,25 @@ impl EmbeddingProvider for OpenAIEmbeddings {
             match response.status() {
                 StatusCode::TOO_MANY_REQUESTS => {
                     let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
+                    log::trace!(
+                        "open ai rate limiting, delaying request by {:?} seconds",
+                        delay.as_secs()
+                    );
                     self.executor.timer(delay).await;
                 }
                 StatusCode::BAD_REQUEST => {
-                    log::info!("BAD REQUEST: {:?}", &response.status());
-                    // Don't worry about delaying bad request, as we can assume
-                    // we haven't been rate limited yet.
-                    for span in spans.iter_mut() {
-                        *span = Self::truncate(span.to_string());
+                    // Only truncate if it hasnt been truncated before
+                    if !truncated {
+                        for span in spans.iter_mut() {
+                            *span = Self::truncate(span.clone());
+                        }
+                        truncated = true;
+                    } else {
+                        // If failing once already truncated, log the error and break the loop
+                        let mut body = String::new();
+                        response.body_mut().read_to_string(&mut body).await?;
+                        log::trace!("open ai bad request: {:?} {:?}", &response.status(), body);
+                        break;
                     }
                 }
                 StatusCode::OK => {
@@ -147,7 +159,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                     response.body_mut().read_to_string(&mut body).await?;
                     let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
 
-                    log::info!(
+                    log::trace!(
                         "openai embedding completed. tokens: {:?}",
                         response.usage.total_tokens
                     );

crates/semantic_index/src/parsing.rs 🔗

@@ -0,0 +1,321 @@
+use anyhow::{anyhow, Ok, Result};
+use language::{Grammar, Language};
+use std::{
+    cmp::{self, Reverse},
+    collections::HashSet,
+    ops::Range,
+    path::Path,
+    sync::Arc,
+};
+use tree_sitter::{Parser, QueryCursor};
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Document {
+    pub name: String,
+    pub range: Range<usize>,
+    pub content: String,
+    pub embedding: Vec<f32>,
+}
+
+const CODE_CONTEXT_TEMPLATE: &str =
+    "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const ENTIRE_FILE_TEMPLATE: &str =
+    "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
+    &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
+
+pub struct CodeContextRetriever {
+    pub parser: Parser,
+    pub cursor: QueryCursor,
+}
+
+// Every match has an item, this represents the fundamental treesitter symbol and anchors the search
+// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication.
+// If there are preceeding comments, we track this with a context capture
+// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture
+// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture
+#[derive(Debug, Clone)]
+pub struct CodeContextMatch {
+    pub start_col: usize,
+    pub item_range: Option<Range<usize>>,
+    pub name_range: Option<Range<usize>>,
+    pub context_ranges: Vec<Range<usize>>,
+    pub collapse_ranges: Vec<Range<usize>>,
+}
+
+impl CodeContextRetriever {
+    pub fn new() -> Self {
+        Self {
+            parser: Parser::new(),
+            cursor: QueryCursor::new(),
+        }
+    }
+
+    fn parse_entire_file(
+        &self,
+        relative_path: &Path,
+        language_name: Arc<str>,
+        content: &str,
+    ) -> Result<Vec<Document>> {
+        let document_span = ENTIRE_FILE_TEMPLATE
+            .replace("<path>", relative_path.to_string_lossy().as_ref())
+            .replace("<language>", language_name.as_ref())
+            .replace("<item>", &content);
+
+        Ok(vec![Document {
+            range: 0..content.len(),
+            content: document_span,
+            embedding: Vec::new(),
+            name: language_name.to_string(),
+        }])
+    }
+
+    fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Document>> {
+        let document_span = MARKDOWN_CONTEXT_TEMPLATE
+            .replace("<path>", relative_path.to_string_lossy().as_ref())
+            .replace("<item>", &content);
+
+        Ok(vec![Document {
+            range: 0..content.len(),
+            content: document_span,
+            embedding: Vec::new(),
+            name: "Markdown".to_string(),
+        }])
+    }
+
+    fn get_matches_in_file(
+        &mut self,
+        content: &str,
+        grammar: &Arc<Grammar>,
+    ) -> Result<Vec<CodeContextMatch>> {
+        let embedding_config = grammar
+            .embedding_config
+            .as_ref()
+            .ok_or_else(|| anyhow!("no embedding queries"))?;
+        self.parser.set_language(grammar.ts_language).unwrap();
+
+        let tree = self
+            .parser
+            .parse(&content, None)
+            .ok_or_else(|| anyhow!("parsing failed"))?;
+
+        let mut captures: Vec<CodeContextMatch> = Vec::new();
+        let mut collapse_ranges: Vec<Range<usize>> = Vec::new();
+        let mut keep_ranges: Vec<Range<usize>> = Vec::new();
+        for mat in self.cursor.matches(
+            &embedding_config.query,
+            tree.root_node(),
+            content.as_bytes(),
+        ) {
+            let mut start_col = 0;
+            let mut item_range: Option<Range<usize>> = None;
+            let mut name_range: Option<Range<usize>> = None;
+            let mut context_ranges: Vec<Range<usize>> = Vec::new();
+            collapse_ranges.clear();
+            keep_ranges.clear();
+            for capture in mat.captures {
+                if capture.index == embedding_config.item_capture_ix {
+                    item_range = Some(capture.node.byte_range());
+                    start_col = capture.node.start_position().column;
+                } else if Some(capture.index) == embedding_config.name_capture_ix {
+                    name_range = Some(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.context_capture_ix {
+                    context_ranges.push(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.collapse_capture_ix {
+                    collapse_ranges.push(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.keep_capture_ix {
+                    keep_ranges.push(capture.node.byte_range());
+                }
+            }
+
+            captures.push(CodeContextMatch {
+                start_col,
+                item_range,
+                name_range,
+                context_ranges,
+                collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges),
+            });
+        }
+        Ok(captures)
+    }
+
+    pub fn parse_file_with_template(
+        &mut self,
+        relative_path: &Path,
+        content: &str,
+        language: Arc<Language>,
+    ) -> Result<Vec<Document>> {
+        let language_name = language.name();
+
+        if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
+            return self.parse_entire_file(relative_path, language_name, &content);
+        } else if &language_name.to_string() == &"Markdown".to_string() {
+            return self.parse_markdown_file(relative_path, &content);
+        }
+
+        let mut documents = self.parse_file(content, language)?;
+        for document in &mut documents {
+            document.content = CODE_CONTEXT_TEMPLATE
+                .replace("<path>", relative_path.to_string_lossy().as_ref())
+                .replace("<language>", language_name.as_ref())
+                .replace("item", &document.content);
+        }
+        Ok(documents)
+    }
+
+    pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Document>> {
+        let grammar = language
+            .grammar()
+            .ok_or_else(|| anyhow!("no grammar for language"))?;
+
+        // Iterate through query matches
+        let matches = self.get_matches_in_file(content, grammar)?;
+
+        let language_scope = language.default_scope();
+        let placeholder = language_scope.collapsed_placeholder();
+
+        let mut documents = Vec::new();
+        let mut collapsed_ranges_within = Vec::new();
+        let mut parsed_name_ranges = HashSet::new();
+        for (i, context_match) in matches.iter().enumerate() {
+            // Items which are collapsible but not embeddable have no item range
+            let item_range = if let Some(item_range) = context_match.item_range.clone() {
+                item_range
+            } else {
+                continue;
+            };
+
+            // Checks for deduplication
+            let name;
+            if let Some(name_range) = context_match.name_range.clone() {
+                name = content
+                    .get(name_range.clone())
+                    .map_or(String::new(), |s| s.to_string());
+                if parsed_name_ranges.contains(&name_range) {
+                    continue;
+                }
+                parsed_name_ranges.insert(name_range);
+            } else {
+                name = String::new();
+            }
+
+            collapsed_ranges_within.clear();
+            'outer: for remaining_match in &matches[(i + 1)..] {
+                for collapsed_range in &remaining_match.collapse_ranges {
+                    if item_range.start <= collapsed_range.start
+                        && item_range.end >= collapsed_range.end
+                    {
+                        collapsed_ranges_within.push(collapsed_range.clone());
+                    } else {
+                        break 'outer;
+                    }
+                }
+            }
+
+            collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end)));
+
+            let mut document_content = String::new();
+            for context_range in &context_match.context_ranges {
+                add_content_from_range(
+                    &mut document_content,
+                    content,
+                    context_range.clone(),
+                    context_match.start_col,
+                );
+                document_content.push_str("\n");
+            }
+
+            let mut offset = item_range.start;
+            for collapsed_range in &collapsed_ranges_within {
+                if collapsed_range.start > offset {
+                    add_content_from_range(
+                        &mut document_content,
+                        content,
+                        offset..collapsed_range.start,
+                        context_match.start_col,
+                    );
+                    offset = collapsed_range.start;
+                }
+
+                if collapsed_range.end > offset {
+                    document_content.push_str(placeholder);
+                    offset = collapsed_range.end;
+                }
+            }
+
+            if offset < item_range.end {
+                add_content_from_range(
+                    &mut document_content,
+                    content,
+                    offset..item_range.end,
+                    context_match.start_col,
+                );
+            }
+
+            documents.push(Document {
+                name,
+                content: document_content,
+                range: item_range.clone(),
+                embedding: vec![],
+            })
+        }
+
+        return Ok(documents);
+    }
+}
+
+pub(crate) fn subtract_ranges(
+    ranges: &[Range<usize>],
+    ranges_to_subtract: &[Range<usize>],
+) -> Vec<Range<usize>> {
+    let mut result = Vec::new();
+
+    let mut ranges_to_subtract = ranges_to_subtract.iter().peekable();
+
+    for range in ranges {
+        let mut offset = range.start;
+
+        while offset < range.end {
+            if let Some(range_to_subtract) = ranges_to_subtract.peek() {
+                if offset < range_to_subtract.start {
+                    let next_offset = cmp::min(range_to_subtract.start, range.end);
+                    result.push(offset..next_offset);
+                    offset = next_offset;
+                } else {
+                    let next_offset = cmp::min(range_to_subtract.end, range.end);
+                    offset = next_offset;
+                }
+
+                if offset >= range_to_subtract.end {
+                    ranges_to_subtract.next();
+                }
+            } else {
+                result.push(offset..range.end);
+                offset = range.end;
+            }
+        }
+    }
+
+    result
+}
+
+fn add_content_from_range(
+    output: &mut String,
+    content: &str,
+    range: Range<usize>,
+    start_col: usize,
+) {
+    for mut line in content.get(range.clone()).unwrap_or("").lines() {
+        for _ in 0..start_col {
+            if line.starts_with(' ') {
+                line = &line[1..];
+            } else {
+                break;
+            }
+        }
+        output.push_str(line);
+        output.push('\n');
+    }
+    output.pop();
+}

crates/semantic_index/src/semantic_index.rs 🔗

@@ -0,0 +1,817 @@
+mod db;
+mod embedding;
+mod parsing;
+pub mod semantic_index_settings;
+
+#[cfg(test)]
+mod semantic_index_tests;
+
+use crate::semantic_index_settings::SemanticIndexSettings;
+use anyhow::{anyhow, Result};
+use db::VectorDatabase;
+use embedding::{EmbeddingProvider, OpenAIEmbeddings};
+use futures::{channel::oneshot, Future};
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
+use language::{Anchor, Buffer, Language, LanguageRegistry};
+use parking_lot::Mutex;
+use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES};
+use postage::watch;
+use project::{search::PathMatcher, Fs, Project, WorktreeId};
+use smol::channel;
+use std::{
+    cmp::Ordering,
+    collections::HashMap,
+    mem,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::{Arc, Weak},
+    time::{Instant, SystemTime},
+};
+use util::{
+    channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
+    http::HttpClient,
+    paths::EMBEDDINGS_DIR,
+    ResultExt,
+};
+
+const SEMANTIC_INDEX_VERSION: usize = 6;
+const EMBEDDINGS_BATCH_SIZE: usize = 80;
+
+pub fn init(
+    fs: Arc<dyn Fs>,
+    http_client: Arc<dyn HttpClient>,
+    language_registry: Arc<LanguageRegistry>,
+    cx: &mut AppContext,
+) {
+    settings::register::<SemanticIndexSettings>(cx);
+
+    let db_file_path = EMBEDDINGS_DIR
+        .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
+        .join("embeddings_db");
+
+    if *RELEASE_CHANNEL == ReleaseChannel::Stable
+        || !settings::get::<SemanticIndexSettings>(cx).enabled
+    {
+        return;
+    }
+
+    cx.spawn(move |mut cx| async move {
+        let semantic_index = SemanticIndex::new(
+            fs,
+            db_file_path,
+            Arc::new(OpenAIEmbeddings {
+                client: http_client,
+                executor: cx.background(),
+            }),
+            language_registry,
+            cx.clone(),
+        )
+        .await?;
+
+        cx.update(|cx| {
+            cx.set_global(semantic_index.clone());
+        });
+
+        anyhow::Ok(())
+    })
+    .detach();
+}
+
+pub struct SemanticIndex {
+    fs: Arc<dyn Fs>,
+    database_url: Arc<PathBuf>,
+    embedding_provider: Arc<dyn EmbeddingProvider>,
+    language_registry: Arc<LanguageRegistry>,
+    db_update_tx: channel::Sender<DbOperation>,
+    parsing_files_tx: channel::Sender<PendingFile>,
+    _db_update_task: Task<()>,
+    _embed_batch_tasks: Vec<Task<()>>,
+    _batch_files_task: Task<()>,
+    _parsing_files_tasks: Vec<Task<()>>,
+    projects: HashMap<WeakModelHandle<Project>, ProjectState>,
+}
+
+struct ProjectState {
+    worktree_db_ids: Vec<(WorktreeId, i64)>,
+    outstanding_job_count_rx: watch::Receiver<usize>,
+    _outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+}
+
+struct JobHandle {
+    tx: Weak<Mutex<watch::Sender<usize>>>,
+}
+
+impl ProjectState {
+    fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
+        self.worktree_db_ids
+            .iter()
+            .find_map(|(worktree_id, db_id)| {
+                if *worktree_id == id {
+                    Some(*db_id)
+                } else {
+                    None
+                }
+            })
+    }
+
+    fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
+        self.worktree_db_ids
+            .iter()
+            .find_map(|(worktree_id, db_id)| {
+                if *db_id == id {
+                    Some(*worktree_id)
+                } else {
+                    None
+                }
+            })
+    }
+}
+
+pub struct PendingFile {
+    worktree_db_id: i64,
+    relative_path: PathBuf,
+    absolute_path: PathBuf,
+    language: Arc<Language>,
+    modified_time: SystemTime,
+    job_handle: JobHandle,
+}
+
+pub struct SearchResult {
+    pub buffer: ModelHandle<Buffer>,
+    pub range: Range<Anchor>,
+}
+
+enum DbOperation {
+    InsertFile {
+        worktree_id: i64,
+        documents: Vec<Document>,
+        path: PathBuf,
+        mtime: SystemTime,
+        job_handle: JobHandle,
+    },
+    Delete {
+        worktree_id: i64,
+        path: PathBuf,
+    },
+    FindOrCreateWorktree {
+        path: PathBuf,
+        sender: oneshot::Sender<Result<i64>>,
+    },
+    FileMTimes {
+        worktree_id: i64,
+        sender: oneshot::Sender<Result<HashMap<PathBuf, SystemTime>>>,
+    },
+    WorktreePreviouslyIndexed {
+        path: Arc<Path>,
+        sender: oneshot::Sender<Result<bool>>,
+    },
+}
+
+enum EmbeddingJob {
+    Enqueue {
+        worktree_id: i64,
+        path: PathBuf,
+        mtime: SystemTime,
+        documents: Vec<Document>,
+        job_handle: JobHandle,
+    },
+    Flush,
+}
+
+impl SemanticIndex {
+    pub fn global(cx: &AppContext) -> Option<ModelHandle<SemanticIndex>> {
+        if cx.has_global::<ModelHandle<Self>>() {
+            Some(cx.global::<ModelHandle<SemanticIndex>>().clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn enabled(cx: &AppContext) -> bool {
+        settings::get::<SemanticIndexSettings>(cx).enabled
+            && *RELEASE_CHANNEL != ReleaseChannel::Stable
+    }
+
+    async fn new(
+        fs: Arc<dyn Fs>,
+        database_url: PathBuf,
+        embedding_provider: Arc<dyn EmbeddingProvider>,
+        language_registry: Arc<LanguageRegistry>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        let t0 = Instant::now();
+        let database_url = Arc::new(database_url);
+
+        let db = cx
+            .background()
+            .spawn(VectorDatabase::new(fs.clone(), database_url.clone()))
+            .await?;
+
+        log::trace!(
+            "db initialization took {:?} milliseconds",
+            t0.elapsed().as_millis()
+        );
+
+        Ok(cx.add_model(|cx| {
+            let t0 = Instant::now();
+            // Perform database operations
+            let (db_update_tx, db_update_rx) = channel::unbounded();
+            let _db_update_task = cx.background().spawn({
+                async move {
+                    while let Ok(job) = db_update_rx.recv().await {
+                        Self::run_db_operation(&db, job)
+                    }
+                }
+            });
+
+            // Group documents into batches and send them to the embedding provider.
+            let (embed_batch_tx, embed_batch_rx) =
+                channel::unbounded::<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>();
+            let mut _embed_batch_tasks = Vec::new();
+            for _ in 0..cx.background().num_cpus() {
+                let embed_batch_rx = embed_batch_rx.clone();
+                _embed_batch_tasks.push(cx.background().spawn({
+                    let db_update_tx = db_update_tx.clone();
+                    let embedding_provider = embedding_provider.clone();
+                    async move {
+                        while let Ok(embeddings_queue) = embed_batch_rx.recv().await {
+                            Self::compute_embeddings_for_batch(
+                                embeddings_queue,
+                                &embedding_provider,
+                                &db_update_tx,
+                            )
+                            .await;
+                        }
+                    }
+                }));
+            }
+
+            // Group documents into batches and send them to the embedding provider.
+            let (batch_files_tx, batch_files_rx) = channel::unbounded::<EmbeddingJob>();
+            let _batch_files_task = cx.background().spawn(async move {
+                let mut queue_len = 0;
+                let mut embeddings_queue = vec![];
+                while let Ok(job) = batch_files_rx.recv().await {
+                    Self::enqueue_documents_to_embed(
+                        job,
+                        &mut queue_len,
+                        &mut embeddings_queue,
+                        &embed_batch_tx,
+                    );
+                }
+            });
+
+            // Parse files into embeddable documents.
+            let (parsing_files_tx, parsing_files_rx) = channel::unbounded::<PendingFile>();
+            let mut _parsing_files_tasks = Vec::new();
+            for _ in 0..cx.background().num_cpus() {
+                let fs = fs.clone();
+                let parsing_files_rx = parsing_files_rx.clone();
+                let batch_files_tx = batch_files_tx.clone();
+                let db_update_tx = db_update_tx.clone();
+                _parsing_files_tasks.push(cx.background().spawn(async move {
+                    let mut retriever = CodeContextRetriever::new();
+                    while let Ok(pending_file) = parsing_files_rx.recv().await {
+                        Self::parse_file(
+                            &fs,
+                            pending_file,
+                            &mut retriever,
+                            &batch_files_tx,
+                            &parsing_files_rx,
+                            &db_update_tx,
+                        )
+                        .await;
+                    }
+                }));
+            }
+
+            log::trace!(
+                "semantic index task initialization took {:?} milliseconds",
+                t0.elapsed().as_millis()
+            );
+            Self {
+                fs,
+                database_url,
+                embedding_provider,
+                language_registry,
+                db_update_tx,
+                parsing_files_tx,
+                _db_update_task,
+                _embed_batch_tasks,
+                _batch_files_task,
+                _parsing_files_tasks,
+                projects: HashMap::new(),
+            }
+        }))
+    }
+
+    fn run_db_operation(db: &VectorDatabase, job: DbOperation) {
+        match job {
+            DbOperation::InsertFile {
+                worktree_id,
+                documents,
+                path,
+                mtime,
+                job_handle,
+            } => {
+                db.insert_file(worktree_id, path, mtime, documents)
+                    .log_err();
+                drop(job_handle)
+            }
+            DbOperation::Delete { worktree_id, path } => {
+                db.delete_file(worktree_id, path).log_err();
+            }
+            DbOperation::FindOrCreateWorktree { path, sender } => {
+                let id = db.find_or_create_worktree(&path);
+                sender.send(id).ok();
+            }
+            DbOperation::FileMTimes {
+                worktree_id: worktree_db_id,
+                sender,
+            } => {
+                let file_mtimes = db.get_file_mtimes(worktree_db_id);
+                sender.send(file_mtimes).ok();
+            }
+            DbOperation::WorktreePreviouslyIndexed { path, sender } => {
+                let worktree_indexed = db.worktree_previously_indexed(path.as_ref());
+                sender.send(worktree_indexed).ok();
+            }
+        }
+    }
+
+    async fn compute_embeddings_for_batch(
+        mut embeddings_queue: Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
+        embedding_provider: &Arc<dyn EmbeddingProvider>,
+        db_update_tx: &channel::Sender<DbOperation>,
+    ) {
+        let mut batch_documents = vec![];
+        for (_, documents, _, _, _) in embeddings_queue.iter() {
+            batch_documents.extend(documents.iter().map(|document| document.content.as_str()));
+        }
+
+        if let Ok(embeddings) = embedding_provider.embed_batch(batch_documents).await {
+            log::trace!(
+                "created {} embeddings for {} files",
+                embeddings.len(),
+                embeddings_queue.len(),
+            );
+
+            let mut i = 0;
+            let mut j = 0;
+
+            for embedding in embeddings.iter() {
+                while embeddings_queue[i].1.len() == j {
+                    i += 1;
+                    j = 0;
+                }
+
+                embeddings_queue[i].1[j].embedding = embedding.to_owned();
+                j += 1;
+            }
+
+            for (worktree_id, documents, path, mtime, job_handle) in embeddings_queue.into_iter() {
+                db_update_tx
+                    .send(DbOperation::InsertFile {
+                        worktree_id,
+                        documents,
+                        path,
+                        mtime,
+                        job_handle,
+                    })
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+
+    fn enqueue_documents_to_embed(
+        job: EmbeddingJob,
+        queue_len: &mut usize,
+        embeddings_queue: &mut Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
+        embed_batch_tx: &channel::Sender<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>,
+    ) {
+        let should_flush = match job {
+            EmbeddingJob::Enqueue {
+                documents,
+                worktree_id,
+                path,
+                mtime,
+                job_handle,
+            } => {
+                *queue_len += &documents.len();
+                embeddings_queue.push((worktree_id, documents, path, mtime, job_handle));
+                *queue_len >= EMBEDDINGS_BATCH_SIZE
+            }
+            EmbeddingJob::Flush => true,
+        };
+
+        if should_flush {
+            embed_batch_tx
+                .try_send(mem::take(embeddings_queue))
+                .unwrap();
+            *queue_len = 0;
+        }
+    }
+
+    async fn parse_file(
+        fs: &Arc<dyn Fs>,
+        pending_file: PendingFile,
+        retriever: &mut CodeContextRetriever,
+        batch_files_tx: &channel::Sender<EmbeddingJob>,
+        parsing_files_rx: &channel::Receiver<PendingFile>,
+        db_update_tx: &channel::Sender<DbOperation>,
+    ) {
+        if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
+            if let Some(documents) = retriever
+                .parse_file_with_template(
+                    &pending_file.relative_path,
+                    &content,
+                    pending_file.language,
+                )
+                .log_err()
+            {
+                log::trace!(
+                    "parsed path {:?}: {} documents",
+                    pending_file.relative_path,
+                    documents.len()
+                );
+
+                if documents.len() == 0 {
+                    db_update_tx
+                        .send(DbOperation::InsertFile {
+                            worktree_id: pending_file.worktree_db_id,
+                            documents,
+                            path: pending_file.relative_path,
+                            mtime: pending_file.modified_time,
+                            job_handle: pending_file.job_handle,
+                        })
+                        .await
+                        .unwrap();
+                } else {
+                    batch_files_tx
+                        .try_send(EmbeddingJob::Enqueue {
+                            worktree_id: pending_file.worktree_db_id,
+                            path: pending_file.relative_path,
+                            mtime: pending_file.modified_time,
+                            job_handle: pending_file.job_handle,
+                            documents,
+                        })
+                        .unwrap();
+                }
+            }
+        }
+
+        if parsing_files_rx.len() == 0 {
+            batch_files_tx.try_send(EmbeddingJob::Flush).unwrap();
+        }
+    }
+
+    fn find_or_create_worktree(&self, path: PathBuf) -> impl Future<Output = Result<i64>> {
+        let (tx, rx) = oneshot::channel();
+        self.db_update_tx
+            .try_send(DbOperation::FindOrCreateWorktree { path, sender: tx })
+            .unwrap();
+        async move { rx.await? }
+    }
+
+    fn get_file_mtimes(
+        &self,
+        worktree_id: i64,
+    ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
+        let (tx, rx) = oneshot::channel();
+        self.db_update_tx
+            .try_send(DbOperation::FileMTimes {
+                worktree_id,
+                sender: tx,
+            })
+            .unwrap();
+        async move { rx.await? }
+    }
+
+    fn worktree_previously_indexed(&self, path: Arc<Path>) -> impl Future<Output = Result<bool>> {
+        let (tx, rx) = oneshot::channel();
+        self.db_update_tx
+            .try_send(DbOperation::WorktreePreviouslyIndexed { path, sender: tx })
+            .unwrap();
+        async move { rx.await? }
+    }
+
+    pub fn project_previously_indexed(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<bool>> {
+        let worktree_scans_complete = project
+            .read(cx)
+            .worktrees(cx)
+            .map(|worktree| {
+                let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
+                async move {
+                    scan_complete.await;
+                }
+            })
+            .collect::<Vec<_>>();
+
+        let worktrees_indexed_previously = project
+            .read(cx)
+            .worktrees(cx)
+            .map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path()))
+            .collect::<Vec<_>>();
+
+        cx.spawn(|_, _cx| async move {
+            futures::future::join_all(worktree_scans_complete).await;
+
+            let worktree_indexed_previously =
+                futures::future::join_all(worktrees_indexed_previously).await;
+
+            Ok(worktree_indexed_previously
+                .iter()
+                .filter(|worktree| worktree.is_ok())
+                .all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned())))
+        })
+    }
+
+    pub fn index_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(usize, watch::Receiver<usize>)>> {
+        let t0 = Instant::now();
+        let worktree_scans_complete = project
+            .read(cx)
+            .worktrees(cx)
+            .map(|worktree| {
+                let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
+                async move {
+                    scan_complete.await;
+                }
+            })
+            .collect::<Vec<_>>();
+        let worktree_db_ids = project
+            .read(cx)
+            .worktrees(cx)
+            .map(|worktree| {
+                self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf())
+            })
+            .collect::<Vec<_>>();
+
+        let language_registry = self.language_registry.clone();
+        let db_update_tx = self.db_update_tx.clone();
+        let parsing_files_tx = self.parsing_files_tx.clone();
+
+        cx.spawn(|this, mut cx| async move {
+            futures::future::join_all(worktree_scans_complete).await;
+
+            let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
+
+            let worktrees = project.read_with(&cx, |project, cx| {
+                project
+                    .worktrees(cx)
+                    .map(|worktree| worktree.read(cx).snapshot())
+                    .collect::<Vec<_>>()
+            });
+
+            let mut worktree_file_mtimes = HashMap::new();
+            let mut db_ids_by_worktree_id = HashMap::new();
+            for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
+                let db_id = db_id?;
+                db_ids_by_worktree_id.insert(worktree.id(), db_id);
+                worktree_file_mtimes.insert(
+                    worktree.id(),
+                    this.read_with(&cx, |this, _| this.get_file_mtimes(db_id))
+                        .await?,
+                );
+            }
+
+            let (job_count_tx, job_count_rx) = watch::channel_with(0);
+            let job_count_tx = Arc::new(Mutex::new(job_count_tx));
+            this.update(&mut cx, |this, _| {
+                this.projects.insert(
+                    project.downgrade(),
+                    ProjectState {
+                        worktree_db_ids: db_ids_by_worktree_id
+                            .iter()
+                            .map(|(a, b)| (*a, *b))
+                            .collect(),
+                        outstanding_job_count_rx: job_count_rx.clone(),
+                        _outstanding_job_count_tx: job_count_tx.clone(),
+                    },
+                );
+            });
+
+            cx.background()
+                .spawn(async move {
+                    let mut count = 0;
+                    for worktree in worktrees.into_iter() {
+                        let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap();
+                        for file in worktree.files(false, 0) {
+                            let absolute_path = worktree.absolutize(&file.path);
+
+                            if let Ok(language) = language_registry
+                                .language_for_file(&absolute_path, None)
+                                .await
+                            {
+                                if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
+                                    && &language.name().as_ref() != &"Markdown"
+                                    && language
+                                        .grammar()
+                                        .and_then(|grammar| grammar.embedding_config.as_ref())
+                                        .is_none()
+                                {
+                                    continue;
+                                }
+
+                                let path_buf = file.path.to_path_buf();
+                                let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
+                                let already_stored = stored_mtime
+                                    .map_or(false, |existing_mtime| existing_mtime == file.mtime);
+
+                                if !already_stored {
+                                    count += 1;
+                                    *job_count_tx.lock().borrow_mut() += 1;
+                                    let job_handle = JobHandle {
+                                        tx: Arc::downgrade(&job_count_tx),
+                                    };
+                                    parsing_files_tx
+                                        .try_send(PendingFile {
+                                            worktree_db_id: db_ids_by_worktree_id[&worktree.id()],
+                                            relative_path: path_buf,
+                                            absolute_path,
+                                            language,
+                                            job_handle,
+                                            modified_time: file.mtime,
+                                        })
+                                        .unwrap();
+                                }
+                            }
+                        }
+                        for file in file_mtimes.keys() {
+                            db_update_tx
+                                .try_send(DbOperation::Delete {
+                                    worktree_id: db_ids_by_worktree_id[&worktree.id()],
+                                    path: file.to_owned(),
+                                })
+                                .unwrap();
+                        }
+                    }
+
+                    log::trace!(
+                        "walking worktree took {:?} milliseconds",
+                        t0.elapsed().as_millis()
+                    );
+                    anyhow::Ok((count, job_count_rx))
+                })
+                .await
+        })
+    }
+
+    pub fn outstanding_job_count_rx(
+        &self,
+        project: &ModelHandle<Project>,
+    ) -> Option<watch::Receiver<usize>> {
+        Some(
+            self.projects
+                .get(&project.downgrade())?
+                .outstanding_job_count_rx
+                .clone(),
+        )
+    }
+
+    pub fn search_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        phrase: String,
+        limit: usize,
+        includes: Vec<PathMatcher>,
+        excludes: Vec<PathMatcher>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        let project_state = if let Some(state) = self.projects.get(&project.downgrade()) {
+            state
+        } else {
+            return Task::ready(Err(anyhow!("project not added")));
+        };
+
+        let worktree_db_ids = project
+            .read(cx)
+            .worktrees(cx)
+            .filter_map(|worktree| {
+                let worktree_id = worktree.read(cx).id();
+                project_state.db_id_for_worktree_id(worktree_id)
+            })
+            .collect::<Vec<_>>();
+
+        let embedding_provider = self.embedding_provider.clone();
+        let database_url = self.database_url.clone();
+        let fs = self.fs.clone();
+        cx.spawn(|this, mut cx| async move {
+            let database = VectorDatabase::new(fs.clone(), database_url.clone()).await?;
+
+            let phrase_embedding = embedding_provider
+                .embed_batch(vec![&phrase])
+                .await?
+                .into_iter()
+                .next()
+                .unwrap();
+
+            let file_ids =
+                database.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)?;
+
+            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 mut result_tasks = Vec::new();
+            for batch in file_ids.chunks(batch_size) {
+                let batch = batch.into_iter().map(|v| *v).collect::<Vec<i64>>();
+                let limit = limit.clone();
+                let fs = fs.clone();
+                let database_url = database_url.clone();
+                let phrase_embedding = phrase_embedding.clone();
+                let task = cx.background().spawn(async move {
+                    let database = VectorDatabase::new(fs, database_url).await.log_err();
+                    if database.is_none() {
+                        return Err(anyhow!("failed to acquire database connection"));
+                    } else {
+                        database
+                            .unwrap()
+                            .top_k_search(&phrase_embedding, limit, batch.as_slice())
+                    }
+                });
+                result_tasks.push(task);
+            }
+
+            let batch_results = futures::future::join_all(result_tasks).await;
+
+            let mut results = Vec::new();
+            for batch_result in batch_results {
+                if batch_result.is_ok() {
+                    for (id, similarity) in batch_result.unwrap() {
+                        let ix = match results.binary_search_by(|(_, s)| {
+                            similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
+                        }) {
+                            Ok(ix) => ix,
+                            Err(ix) => ix,
+                        };
+                        results.insert(ix, (id, similarity));
+                        results.truncate(limit);
+                    }
+                }
+            }
+
+            let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<i64>>();
+            let documents = database.get_documents_by_ids(ids.as_slice())?;
+
+            let mut tasks = Vec::new();
+            let mut ranges = Vec::new();
+            let weak_project = project.downgrade();
+            project.update(&mut cx, |project, cx| {
+                for (worktree_db_id, file_path, byte_range) in documents {
+                    let project_state =
+                        if let Some(state) = this.read(cx).projects.get(&weak_project) {
+                            state
+                        } else {
+                            return Err(anyhow!("project not added"));
+                        };
+                    if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) {
+                        tasks.push(project.open_buffer((worktree_id, file_path), cx));
+                        ranges.push(byte_range);
+                    }
+                }
+
+                Ok(())
+            })?;
+
+            let buffers = futures::future::join_all(tasks).await;
+
+            Ok(buffers
+                .into_iter()
+                .zip(ranges)
+                .filter_map(|(buffer, range)| {
+                    let buffer = buffer.log_err()?;
+                    let range = buffer.read_with(&cx, |buffer, _| {
+                        buffer.anchor_before(range.start)..buffer.anchor_after(range.end)
+                    });
+                    Some(SearchResult { buffer, range })
+                })
+                .collect::<Vec<_>>())
+        })
+    }
+}
+
+impl Entity for SemanticIndex {
+    type Event = ();
+}
+
+impl Drop for JobHandle {
+    fn drop(&mut self) {
+        if let Some(tx) = self.tx.upgrade() {
+            let mut tx = tx.lock();
+            *tx.borrow_mut() -= 1;
+        }
+    }
+}

crates/vector_store/src/vector_store_settings.rs → crates/semantic_index/src/semantic_index_settings.rs 🔗

@@ -4,21 +4,21 @@ use serde::{Deserialize, Serialize};
 use settings::Setting;
 
 #[derive(Deserialize, Debug)]
-pub struct VectorStoreSettings {
+pub struct SemanticIndexSettings {
     pub enabled: bool,
     pub reindexing_delay_seconds: usize,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct VectorStoreSettingsContent {
+pub struct SemanticIndexSettingsContent {
     pub enabled: Option<bool>,
     pub reindexing_delay_seconds: Option<usize>,
 }
 
-impl Setting for VectorStoreSettings {
-    const KEY: Option<&'static str> = Some("vector_store");
+impl Setting for SemanticIndexSettings {
+    const KEY: Option<&'static str> = Some("semantic_index");
 
-    type FileContent = VectorStoreSettingsContent;
+    type FileContent = SemanticIndexSettingsContent;
 
     fn load(
         default_value: &Self::FileContent,

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -0,0 +1,1679 @@
+use crate::{
+    db::dot,
+    embedding::EmbeddingProvider,
+    parsing::{subtract_ranges, CodeContextRetriever, Document},
+    semantic_index_settings::SemanticIndexSettings,
+    SearchResult, SemanticIndex,
+};
+use anyhow::Result;
+use async_trait::async_trait;
+use gpui::{Task, TestAppContext};
+use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
+use pretty_assertions::assert_eq;
+use project::{project_settings::ProjectSettings, search::PathMatcher, FakeFs, Fs, Project};
+use rand::{rngs::StdRng, Rng};
+use serde_json::json;
+use settings::SettingsStore;
+use std::{
+    path::Path,
+    sync::{
+        atomic::{self, AtomicUsize},
+        Arc,
+    },
+};
+use unindent::Unindent;
+
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
+#[gpui::test]
+async fn test_semantic_index(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        settings::register::<SemanticIndexSettings>(cx);
+        settings::register::<ProjectSettings>(cx);
+    });
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "src": {
+                "file1.rs": "
+                    fn aaa() {
+                        println!(\"aaaaaaaaaaaa!\");
+                    }
+
+                    fn zzzzz() {
+                        println!(\"SLEEPING\");
+                    }
+                ".unindent(),
+                "file2.rs": "
+                    fn bbb() {
+                        println!(\"bbbbbbbbbbbbb!\");
+                    }
+                ".unindent(),
+                "file3.toml": "
+                    ZZZZZZZZZZZZZZZZZZ = 5
+                ".unindent(),
+            }
+        }),
+    )
+    .await;
+
+    let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
+    let rust_language = rust_lang();
+    let toml_language = toml_lang();
+    languages.add(rust_language);
+    languages.add(toml_language);
+
+    let db_dir = tempdir::TempDir::new("vector-store").unwrap();
+    let db_path = db_dir.path().join("db.sqlite");
+
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let store = SemanticIndex::new(
+        fs.clone(),
+        db_path,
+        embedding_provider.clone(),
+        languages,
+        cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    let (file_count, outstanding_file_count) = store
+        .update(cx, |store, cx| store.index_project(project.clone(), cx))
+        .await
+        .unwrap();
+    assert_eq!(file_count, 3);
+    cx.foreground().run_until_parked();
+    assert_eq!(*outstanding_file_count.borrow(), 0);
+
+    let search_results = store
+        .update(cx, |store, cx| {
+            store.search_project(
+                project.clone(),
+                "aaaaaabbbbzz".to_string(),
+                5,
+                vec![],
+                vec![],
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    assert_search_results(
+        &search_results,
+        &[
+            (Path::new("src/file1.rs").into(), 0),
+            (Path::new("src/file2.rs").into(), 0),
+            (Path::new("src/file3.toml").into(), 0),
+            (Path::new("src/file1.rs").into(), 45),
+        ],
+        cx,
+    );
+
+    // Test Include Files Functonality
+    let include_files = vec![PathMatcher::new("*.rs").unwrap()];
+    let exclude_files = vec![PathMatcher::new("*.rs").unwrap()];
+    let rust_only_search_results = store
+        .update(cx, |store, cx| {
+            store.search_project(
+                project.clone(),
+                "aaaaaabbbbzz".to_string(),
+                5,
+                include_files,
+                vec![],
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    assert_search_results(
+        &rust_only_search_results,
+        &[
+            (Path::new("src/file1.rs").into(), 0),
+            (Path::new("src/file2.rs").into(), 0),
+            (Path::new("src/file1.rs").into(), 45),
+        ],
+        cx,
+    );
+
+    let no_rust_search_results = store
+        .update(cx, |store, cx| {
+            store.search_project(
+                project.clone(),
+                "aaaaaabbbbzz".to_string(),
+                5,
+                vec![],
+                exclude_files,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    assert_search_results(
+        &no_rust_search_results,
+        &[(Path::new("src/file3.toml").into(), 0)],
+        cx,
+    );
+
+    fs.save(
+        "/the-root/src/file2.rs".as_ref(),
+        &"
+            fn dddd() { println!(\"ddddd!\"); }
+            struct pqpqpqp {}
+        "
+        .unindent()
+        .into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+
+    cx.foreground().run_until_parked();
+
+    let prev_embedding_count = embedding_provider.embedding_count();
+    let (file_count, outstanding_file_count) = store
+        .update(cx, |store, cx| store.index_project(project.clone(), cx))
+        .await
+        .unwrap();
+    assert_eq!(file_count, 1);
+
+    cx.foreground().run_until_parked();
+    assert_eq!(*outstanding_file_count.borrow(), 0);
+
+    assert_eq!(
+        embedding_provider.embedding_count() - prev_embedding_count,
+        2
+    );
+}
+
+#[track_caller]
+fn assert_search_results(
+    actual: &[SearchResult],
+    expected: &[(Arc<Path>, usize)],
+    cx: &TestAppContext,
+) {
+    let actual = actual
+        .iter()
+        .map(|search_result| {
+            search_result.buffer.read_with(cx, |buffer, _cx| {
+                (
+                    buffer.file().unwrap().path().clone(),
+                    search_result.range.start.to_offset(buffer),
+                )
+            })
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(actual, expected);
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_rust() {
+    let language = rust_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = "
+        /// A doc comment
+        /// that spans multiple lines
+        #[gpui::test]
+        fn a() {
+            b
+        }
+
+        impl C for D {
+        }
+
+        impl E {
+            // This is also a preceding comment
+            pub fn function_1() -> Option<()> {
+                todo!();
+            }
+
+            // This is a preceding comment
+            fn function_2() -> Result<()> {
+                todo!();
+            }
+        }
+    "
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+                /// A doc comment
+                /// that spans multiple lines
+                #[gpui::test]
+                fn a() {
+                    b
+                }"
+                .unindent(),
+                text.find("fn a").unwrap(),
+            ),
+            (
+                "
+                impl C for D {
+                }"
+                .unindent(),
+                text.find("impl C").unwrap(),
+            ),
+            (
+                "
+                impl E {
+                    // This is also a preceding comment
+                    pub fn function_1() -> Option<()> { /* ... */ }
+
+                    // This is a preceding comment
+                    fn function_2() -> Result<()> { /* ... */ }
+                }"
+                .unindent(),
+                text.find("impl E").unwrap(),
+            ),
+            (
+                "
+                // This is also a preceding comment
+                pub fn function_1() -> Option<()> {
+                    todo!();
+                }"
+                .unindent(),
+                text.find("pub fn function_1").unwrap(),
+            ),
+            (
+                "
+                // This is a preceding comment
+                fn function_2() -> Result<()> {
+                    todo!();
+                }"
+                .unindent(),
+                text.find("fn function_2").unwrap(),
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_json() {
+    let language = json_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        {
+            "array": [1, 2, 3, 4],
+            "string": "abcdefg",
+            "nested_object": {
+                "array_2": [5, 6, 7, 8],
+                "string_2": "hijklmnop",
+                "boolean": true,
+                "none": null
+            }
+        }
+    "#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+                {
+                    "array": [],
+                    "string": "",
+                    "nested_object": {
+                        "array_2": [],
+                        "string_2": "",
+                        "boolean": true,
+                        "none": null
+                    }
+                }"#
+            .unindent(),
+            text.find("{").unwrap(),
+        )],
+    );
+
+    let text = r#"
+        [
+            {
+                "name": "somebody",
+                "age": 42
+            },
+            {
+                "name": "somebody else",
+                "age": 43
+            }
+        ]
+    "#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+            [{
+                    "name": "",
+                    "age": 42
+                }]"#
+            .unindent(),
+            text.find("[").unwrap(),
+        )],
+    );
+}
+
+fn assert_documents_eq(
+    documents: &[Document],
+    expected_contents_and_start_offsets: &[(String, usize)],
+) {
+    assert_eq!(
+        documents
+            .iter()
+            .map(|document| (document.content.clone(), document.range.start))
+            .collect::<Vec<_>>(),
+        expected_contents_and_start_offsets
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_javascript() {
+    let language = js_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = "
+        /* globals importScripts, backend */
+        function _authorize() {}
+
+        /**
+         * Sometimes the frontend build is way faster than backend.
+         */
+        export async function authorizeBank() {
+            _authorize(pushModal, upgradingAccountId, {});
+        }
+
+        export class SettingsPage {
+            /* This is a test setting */
+            constructor(page) {
+                this.page = page;
+            }
+        }
+
+        /* This is a test comment */
+        class TestClass {}
+
+        /* Schema for editor_events in Clickhouse. */
+        export interface ClickhouseEditorEvent {
+            installation_id: string
+            operation: string
+        }
+        "
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+            /* globals importScripts, backend */
+            function _authorize() {}"
+                    .unindent(),
+                37,
+            ),
+            (
+                "
+            /**
+             * Sometimes the frontend build is way faster than backend.
+             */
+            export async function authorizeBank() {
+                _authorize(pushModal, upgradingAccountId, {});
+            }"
+                .unindent(),
+                131,
+            ),
+            (
+                "
+                export class SettingsPage {
+                    /* This is a test setting */
+                    constructor(page) {
+                        this.page = page;
+                    }
+                }"
+                .unindent(),
+                225,
+            ),
+            (
+                "
+                /* This is a test setting */
+                constructor(page) {
+                    this.page = page;
+                }"
+                .unindent(),
+                290,
+            ),
+            (
+                "
+                /* This is a test comment */
+                class TestClass {}"
+                    .unindent(),
+                374,
+            ),
+            (
+                "
+                /* Schema for editor_events in Clickhouse. */
+                export interface ClickhouseEditorEvent {
+                    installation_id: string
+                    operation: string
+                }"
+                .unindent(),
+                440,
+            ),
+        ],
+    )
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_lua() {
+    let language = lua_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        -- Creates a new class
+        -- @param baseclass The Baseclass of this class, or nil.
+        -- @return A new class reference.
+        function classes.class(baseclass)
+            -- Create the class definition and metatable.
+            local classdef = {}
+            -- Find the super class, either Object or user-defined.
+            baseclass = baseclass or classes.Object
+            -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+            setmetatable(classdef, { __index = baseclass })
+            -- All class instances have a reference to the class object.
+            classdef.class = classdef
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end
+        end
+        "#.unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (r#"
+                -- Creates a new class
+                -- @param baseclass The Baseclass of this class, or nil.
+                -- @return A new class reference.
+                function classes.class(baseclass)
+                    -- Create the class definition and metatable.
+                    local classdef = {}
+                    -- Find the super class, either Object or user-defined.
+                    baseclass = baseclass or classes.Object
+                    -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+                    setmetatable(classdef, { __index = baseclass })
+                    -- All class instances have a reference to the class object.
+                    classdef.class = classdef
+                    --- Recursivly allocates the inheritance tree of the instance.
+                    -- @param mastertable The 'root' of the inheritance tree.
+                    -- @return Returns the instance with the allocated inheritance tree.
+                    function classdef.alloc(mastertable)
+                        --[ ... ]--
+                        --[ ... ]--
+                    end
+                end"#.unindent(),
+            114),
+            (r#"
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end"#.unindent(), 809),
+        ]
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_elixir() {
+    let language = elixir_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        defmodule File.Stream do
+            @moduledoc """
+            Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+            The following fields are public:
+
+            * `path`          - the file path
+            * `modes`         - the file modes
+            * `raw`           - a boolean indicating if bin functions should be used
+            * `line_or_bytes` - if reading should read lines or a given number of bytes
+            * `node`          - the node the file belongs to
+
+            """
+
+            defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+            @type t :: %__MODULE__{}
+
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+        defmodule File.Stream do
+            @moduledoc """
+            Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+            The following fields are public:
+
+            * `path`          - the file path
+            * `modes`         - the file modes
+            * `raw`           - a boolean indicating if bin functions should be used
+            * `line_or_bytes` - if reading should read lines or a given number of bytes
+            * `node`          - the node the file belongs to
+
+            """
+
+            defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+            @type t :: %__MODULE__{}
+
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#
+                .unindent(),
+            0,
+        ),(r#"
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#.unindent(), 574)],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_cpp() {
+    let language = cpp_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = "
+    /**
+     * @brief Main function
+     * @returns 0 on exit
+     */
+    int main() { return 0; }
+
+    /**
+    * This is a test comment
+    */
+    class MyClass {       // The class
+        public:           // Access specifier
+        int myNum;        // Attribute (int variable)
+        string myString;  // Attribute (string variable)
+    };
+
+    // This is a test comment
+    enum Color { red, green, blue };
+
+    /** This is a preceding block comment
+     * This is the second line
+     */
+    struct {           // Structure declaration
+        int myNum;       // Member (int variable)
+        string myString; // Member (string variable)
+    } myStructure;
+
+    /**
+     * @brief Matrix class.
+     */
+    template <typename T,
+              typename = typename std::enable_if<
+                std::is_integral<T>::value || std::is_floating_point<T>::value,
+                bool>::type>
+    class Matrix2 {
+        std::vector<std::vector<T>> _mat;
+
+        public:
+            /**
+            * @brief Constructor
+            * @tparam Integer ensuring integers are being evaluated and not other
+            * data types.
+            * @param size denoting the size of Matrix as size x size
+            */
+            template <typename Integer,
+                    typename = typename std::enable_if<std::is_integral<Integer>::value,
+                    Integer>::type>
+            explicit Matrix(const Integer size) {
+                for (size_t i = 0; i < size; ++i) {
+                    _mat.emplace_back(std::vector<T>(size, 0));
+                }
+            }
+    }"
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+        /**
+         * @brief Main function
+         * @returns 0 on exit
+         */
+        int main() { return 0; }"
+                    .unindent(),
+                54,
+            ),
+            (
+                "
+                /**
+                * This is a test comment
+                */
+                class MyClass {       // The class
+                    public:           // Access specifier
+                    int myNum;        // Attribute (int variable)
+                    string myString;  // Attribute (string variable)
+                }"
+                .unindent(),
+                112,
+            ),
+            (
+                "
+                // This is a test comment
+                enum Color { red, green, blue }"
+                    .unindent(),
+                322,
+            ),
+            (
+                "
+                /** This is a preceding block comment
+                 * This is the second line
+                 */
+                struct {           // Structure declaration
+                    int myNum;       // Member (int variable)
+                    string myString; // Member (string variable)
+                } myStructure;"
+                    .unindent(),
+                425,
+            ),
+            (
+                "
+                /**
+                 * @brief Matrix class.
+                 */
+                template <typename T,
+                          typename = typename std::enable_if<
+                            std::is_integral<T>::value || std::is_floating_point<T>::value,
+                            bool>::type>
+                class Matrix2 {
+                    std::vector<std::vector<T>> _mat;
+
+                    public:
+                        /**
+                        * @brief Constructor
+                        * @tparam Integer ensuring integers are being evaluated and not other
+                        * data types.
+                        * @param size denoting the size of Matrix as size x size
+                        */
+                        template <typename Integer,
+                                typename = typename std::enable_if<std::is_integral<Integer>::value,
+                                Integer>::type>
+                        explicit Matrix(const Integer size) {
+                            for (size_t i = 0; i < size; ++i) {
+                                _mat.emplace_back(std::vector<T>(size, 0));
+                            }
+                        }
+                }"
+                .unindent(),
+                612,
+            ),
+            (
+                "
+                explicit Matrix(const Integer size) {
+                    for (size_t i = 0; i < size; ++i) {
+                        _mat.emplace_back(std::vector<T>(size, 0));
+                    }
+                }"
+                .unindent(),
+                1226,
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_ruby() {
+    let language = ruby_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end
+
+            def challenge_passed?
+                current_user.valid_password?(challenge_params[:current_password])
+            end
+        end
+
+        class Animal
+            include Comparable
+
+            attr_reader :legs
+
+            def initialize(name, legs)
+                @name, @legs = name, legs
+            end
+
+            def <=>(other)
+                legs <=> other.legs
+            end
+        end
+
+        # Singleton method for car object
+        def car.wheels
+            puts "There are four wheels"
+        end"#
+        .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                # ...
+            end
+
+            def challenge_passed?
+                # ...
+            end
+        end"#
+                    .unindent(),
+                558,
+            ),
+            (
+                r#"
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end"#
+                    .unindent(),
+                663,
+            ),
+            (
+                r#"
+                def challenge_passed?
+                    current_user.valid_password?(challenge_params[:current_password])
+                end"#
+                    .unindent(),
+                1254,
+            ),
+            (
+                r#"
+                class Animal
+                    include Comparable
+
+                    attr_reader :legs
+
+                    def initialize(name, legs)
+                        # ...
+                    end
+
+                    def <=>(other)
+                        # ...
+                    end
+                end"#
+                    .unindent(),
+                1363,
+            ),
+            (
+                r#"
+                def initialize(name, legs)
+                    @name, @legs = name, legs
+                end"#
+                    .unindent(),
+                1427,
+            ),
+            (
+                r#"
+                def <=>(other)
+                    legs <=> other.legs
+                end"#
+                    .unindent(),
+                1501,
+            ),
+            (
+                r#"
+                # Singleton method for car object
+                def car.wheels
+                    puts "There are four wheels"
+                end"#
+                    .unindent(),
+                1591,
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_php() {
+    let language = php_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        <?php
+
+        namespace LevelUp\Experience\Concerns;
+
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }
+
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }
+
+            public function achievements(): BelongsToMany
+            {
+                return $this->belongsToMany(related: Achievement::class)
+                ->withPivot(columns: 'progress')
+                ->where('is_secret', false)
+                ->using(AchievementUser::class);
+            }
+        }
+
+        interface Multiplier
+        {
+            public function qualifies(array $data): bool;
+
+            public function setMultiplier(): int;
+        }
+
+        enum AuditType: string
+        {
+            case Add = 'add';
+            case Remove = 'remove';
+            case Reset = 'reset';
+            case LevelUp = 'level_up';
+        }
+
+        ?>"#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }"#
+                .unindent(),
+                123,
+            ),
+            (
+                r#"
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {/* ... */}
+
+            public function achievements(): BelongsToMany
+            {/* ... */}
+        }"#
+                .unindent(),
+                177,
+            ),
+            (r#"
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }"#.unindent(), 245),
+            (r#"
+                public function achievements(): BelongsToMany
+                {
+                    return $this->belongsToMany(related: Achievement::class)
+                    ->withPivot(columns: 'progress')
+                    ->where('is_secret', false)
+                    ->using(AchievementUser::class);
+                }"#.unindent(), 902),
+            (r#"
+                interface Multiplier
+                {
+                    public function qualifies(array $data): bool;
+
+                    public function setMultiplier(): int;
+                }"#.unindent(),
+                1146),
+            (r#"
+                enum AuditType: string
+                {
+                    case Add = 'add';
+                    case Remove = 'remove';
+                    case Reset = 'reset';
+                    case LevelUp = 'level_up';
+                }"#.unindent(), 1265)
+        ],
+    );
+}
+
+#[gpui::test]
+fn test_dot_product(mut rng: StdRng) {
+    assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.);
+    assert_eq!(dot(&[2., 0., 0., 0., 0.], &[3., 1., 0., 0., 0.]), 6.);
+
+    for _ in 0..100 {
+        let size = 1536;
+        let mut a = vec![0.; size];
+        let mut b = vec![0.; size];
+        for (a, b) in a.iter_mut().zip(b.iter_mut()) {
+            *a = rng.gen();
+            *b = rng.gen();
+        }
+
+        assert_eq!(
+            round_to_decimals(dot(&a, &b), 1),
+            round_to_decimals(reference_dot(&a, &b), 1)
+        );
+    }
+
+    fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
+        let factor = (10.0 as f32).powi(decimal_places);
+        (n * factor).round() / factor
+    }
+
+    fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
+        a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
+    }
+}
+
+#[derive(Default)]
+struct FakeEmbeddingProvider {
+    embedding_count: AtomicUsize,
+}
+
+impl FakeEmbeddingProvider {
+    fn embedding_count(&self) -> usize {
+        self.embedding_count.load(atomic::Ordering::SeqCst)
+    }
+}
+
+#[async_trait]
+impl EmbeddingProvider for FakeEmbeddingProvider {
+    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
+        self.embedding_count
+            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
+        Ok(spans
+            .iter()
+            .map(|span| {
+                let mut result = vec![1.0; 26];
+                for letter in span.chars() {
+                    let letter = letter.to_ascii_lowercase();
+                    if letter as u32 >= 'a' as u32 {
+                        let ix = (letter as u32) - ('a' as u32);
+                        if ix < 26 {
+                            result[ix as usize] += 1.0;
+                        }
+                    }
+                }
+
+                let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
+                for x in &mut result {
+                    *x /= norm;
+                }
+
+                result
+            })
+            .collect())
+    }
+}
+
+fn js_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Javascript".into(),
+                path_suffixes: vec!["js".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_embedding_query(
+            &r#"
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (function_declaration
+                        "async"? @name
+                        "function" @name
+                        name: (_) @name))
+                (function_declaration
+                    "async"? @name
+                    "function" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (class_declaration
+                        "class" @name
+                        name: (_) @name))
+                (class_declaration
+                    "class" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (interface_declaration
+                        "interface" @name
+                        name: (_) @name))
+                (interface_declaration
+                    "interface" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (enum_declaration
+                        "enum" @name
+                        name: (_) @name))
+                (enum_declaration
+                    "enum" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                (method_definition
+                    [
+                        "get"
+                        "set"
+                        "async"
+                        "*"
+                        "static"
+                    ]* @name
+                    name: (_) @name) @item
+            )
+
+                    "#
+            .unindent(),
+        )
+        .unwrap(),
+    )
+}
+
+fn rust_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".into()],
+                collapsed_placeholder: " /* ... */ ".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                [(line_comment) (attribute_item)]* @context
+                .
+                [
+                    (struct_item
+                        name: (_) @name)
+
+                    (enum_item
+                        name: (_) @name)
+
+                    (impl_item
+                        trait: (_)? @name
+                        "for"? @name
+                        type: (_) @name)
+
+                    (trait_item
+                        name: (_) @name)
+
+                    (function_item
+                        name: (_) @name
+                        body: (block
+                            "{" @keep
+                            "}" @keep) @collapse)
+
+                    (macro_definition
+                        name: (_) @name)
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn json_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "JSON".into(),
+                path_suffixes: vec!["json".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_json::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (document) @item
+
+            (array
+                "[" @keep
+                .
+                (object)? @keep
+                "]" @keep) @collapse
+
+            (pair value: (string
+                "\"" @keep
+                "\"" @keep) @collapse)
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn toml_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "TOML".into(),
+            path_suffixes: vec!["toml".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_toml::language()),
+    ))
+}
+
+fn cpp_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "CPP".into(),
+                path_suffixes: vec!["cpp".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_cpp::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                (function_definition
+                    (type_qualifier)? @name
+                    type: (_)? @name
+                    declarator: [
+                        (function_declarator
+                            declarator: (_) @name)
+                        (pointer_declarator
+                            "*" @name
+                            declarator: (function_declarator
+                            declarator: (_) @name))
+                        (pointer_declarator
+                            "*" @name
+                            declarator: (pointer_declarator
+                                "*" @name
+                            declarator: (function_declarator
+                                declarator: (_) @name)))
+                        (reference_declarator
+                            ["&" "&&"] @name
+                            (function_declarator
+                            declarator: (_) @name))
+                    ]
+                    (type_qualifier)? @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (template_declaration
+                    (class_specifier
+                        "class" @name
+                        name: (_) @name)
+                        ) @item
+            )
+
+            (
+                (comment)* @context
+                .
+                (class_specifier
+                    "class" @name
+                    name: (_) @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (enum_specifier
+                    "enum" @name
+                    name: (_) @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (declaration
+                    type: (struct_specifier
+                    "struct" @name)
+                    declarator: (_) @name) @item
+            )
+
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn lua_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Lua".into(),
+                path_suffixes: vec!["lua".into()],
+                collapsed_placeholder: "--[ ... ]--".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_lua::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                (function_declaration
+                    "function" @name
+                    name: (_) @name
+                    (comment)* @collapse
+                    body: (block) @collapse
+                ) @item
+            )
+        "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn php_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "PHP".into(),
+                path_suffixes: vec!["php".into()],
+                collapsed_placeholder: "/* ... */".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_php::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                    (function_definition
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (trait_declaration
+                        "trait" @name
+                        name: (_) @name)
+
+                    (method_declaration
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (interface_declaration
+                        "interface" @name
+                        name: (_) @name
+                        )
+
+                    (enum_declaration
+                        "enum" @name
+                        name: (_) @name
+                        )
+
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn ruby_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Ruby".into(),
+                path_suffixes: vec!["rb".into()],
+                collapsed_placeholder: "# ...".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_ruby::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                (module
+                    "module" @name
+                    name: (_) @name)
+                (method
+                    "def" @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                (class
+                    "class" @name
+                    name: (_) @name)
+                (singleton_method
+                    "def" @name
+                    object: (_) @name
+                    "." @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn elixir_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Elixir".into(),
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_elixir::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (unary_operator
+                    operator: "@"
+                    operand: (call
+                        target: (identifier) @unary
+                        (#match? @unary "^(doc)$"))
+                    ) @context
+                .
+                (call
+                target: (identifier) @name
+                (arguments
+                [
+                (identifier) @name
+                (call
+                target: (identifier) @name)
+                (binary_operator
+                left: (call
+                target: (identifier) @name)
+                operator: "when")
+                ])
+                (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+                )
+
+            (call
+                target: (identifier) @name
+                (arguments (alias) @name)
+                (#match? @name "^(defmodule|defprotocol)$")) @item
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+#[gpui::test]
+fn test_subtract_ranges() {
+    // collapsed_ranges: Vec<Range<usize>>, keep_ranges: Vec<Range<usize>>
+
+    assert_eq!(
+        subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]),
+        vec![1..4, 10..21]
+    );
+
+    assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]);
+}

crates/sum_tree/src/cursor.rs 🔗

@@ -202,7 +202,7 @@ where
                 self.position = D::default();
             }
 
-            let mut entry = self.stack.last_mut().unwrap();
+            let entry = self.stack.last_mut().unwrap();
             if !descending {
                 if entry.index == 0 {
                     self.stack.pop();
@@ -438,6 +438,7 @@ where
                 } => {
                     if ascending {
                         entry.index += 1;
+                        entry.position = self.position.clone();
                     }
 
                     for (child_tree, child_summary) in child_trees[entry.index..]

crates/sum_tree/src/sum_tree.rs 🔗

@@ -738,7 +738,7 @@ mod tests {
             for _ in 0..num_operations {
                 let splice_end = rng.gen_range(0..tree.extent::<Count>(&()).0 + 1);
                 let splice_start = rng.gen_range(0..splice_end + 1);
-                let count = rng.gen_range(0..3);
+                let count = rng.gen_range(0..10);
                 let tree_end = tree.extent::<Count>(&());
                 let new_items = rng
                     .sample_iter(distributions::Standard)
@@ -805,10 +805,12 @@ mod tests {
                 }
                 assert_eq!(filter_cursor.item(), None);
 
-                let mut pos = rng.gen_range(0..tree.extent::<Count>(&()).0 + 1);
                 let mut before_start = false;
                 let mut cursor = tree.cursor::<Count>();
-                cursor.seek(&Count(pos), Bias::Right, &());
+                let start_pos = rng.gen_range(0..=reference_items.len());
+                cursor.seek(&Count(start_pos), Bias::Right, &());
+                let mut pos = rng.gen_range(start_pos..=reference_items.len());
+                cursor.seek_forward(&Count(pos), Bias::Right, &());
 
                 for i in 0..10 {
                     assert_eq!(cursor.start().0, pos);

crates/terminal/Cargo.toml 🔗

@@ -16,7 +16,7 @@ db = { path = "../db" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 
-alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
+alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "7b9f32300ee0a249c0872302c97635b460e45ba5" }
 procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
 smallvec.workspace = true
 smol.workspace = true

crates/terminal/src/mappings/colors.rs 🔗

@@ -114,11 +114,7 @@ fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
 
 //Convenience method to convert from a GPUI color to an alacritty Rgb
 pub fn to_alac_rgb(color: Color) -> AlacRgb {
-    AlacRgb {
-        r: color.r,
-        g: color.g,
-        b: color.g,
-    }
+    AlacRgb::new(color.r, color.g, color.g)
 }
 
 #[cfg(test)]

crates/terminal/src/terminal.rs 🔗

@@ -53,7 +53,7 @@ use gpui::{
     keymap_matcher::Keystroke,
     platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
     scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
-    AppContext, ClipboardItem, Entity, ModelContext, Task,
+    AnyWindowHandle, AppContext, ClipboardItem, Entity, ModelContext, Task,
 };
 
 use crate::mappings::{
@@ -78,7 +78,7 @@ lazy_static! {
     // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
     static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
 
-    static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-~]+").unwrap();
+    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap();
 }
 
 ///Upward flowing events, for changing the title and such
@@ -404,7 +404,7 @@ impl TerminalBuilder {
         mut env: HashMap<String, String>,
         blink_settings: Option<TerminalBlink>,
         alternate_scroll: AlternateScroll,
-        window_id: usize,
+        window: AnyWindowHandle,
     ) -> Result<TerminalBuilder> {
         let pty_config = {
             let alac_shell = match shell.clone() {
@@ -462,7 +462,7 @@ impl TerminalBuilder {
         let pty = match tty::new(
             &pty_config,
             TerminalSize::default().into(),
-            window_id as u64,
+            window.id() as u64,
         ) {
             Ok(pty) => pty,
             Err(error) => {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -412,6 +412,10 @@ impl TerminalElement {
             })
             // Update drag selections
             .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
+                if event.end {
+                    return;
+                }
+
                 if cx.is_self_focused() {
                     if let Some(conn_handle) = connection.upgrade(cx) {
                         conn_handle.update(cx, |terminal, cx| {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -48,7 +48,7 @@ impl TerminalPanel {
     fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         let weak_self = cx.weak_handle();
         let pane = cx.add_view(|cx| {
-            let window_id = cx.window_id();
+            let window = cx.window();
             let mut pane = Pane::new(
                 workspace.weak_handle(),
                 workspace.project().clone(),
@@ -60,7 +60,7 @@ impl TerminalPanel {
             pane.set_can_navigate(false, cx);
             pane.on_can_drop(move |drag_and_drop, cx| {
                 drag_and_drop
-                    .currently_dragged::<DraggedItem>(window_id)
+                    .currently_dragged::<DraggedItem>(window)
                     .map_or(false, |(_, item)| {
                         item.handle.act_as::<TerminalView>(cx).is_some()
                     })
@@ -255,10 +255,10 @@ impl TerminalPanel {
                     .clone();
                 let working_directory =
                     crate::get_working_directory(workspace, cx, working_directory_strategy);
-                let window_id = cx.window_id();
+                let window = cx.window();
                 if let Some(terminal) = workspace.project().update(cx, |project, cx| {
                     project
-                        .create_terminal(working_directory, window_id, cx)
+                        .create_terminal(working_directory, window, cx)
                         .log_err()
                 }) {
                     let terminal = Box::new(cx.add_view(|cx| {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -112,11 +112,11 @@ impl TerminalView {
         let working_directory =
             get_working_directory(workspace, cx, strategy.working_directory.clone());
 
-        let window_id = cx.window_id();
+        let window = cx.window();
         let terminal = workspace
             .project()
             .update(cx, |project, cx| {
-                project.create_terminal(working_directory, window_id, cx)
+                project.create_terminal(working_directory, window, cx)
             })
             .notify_err(workspace, cx);
 
@@ -741,7 +741,7 @@ impl Item for TerminalView {
         item_id: workspace::ItemId,
         cx: &mut ViewContext<Pane>,
     ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.spawn(|pane, mut cx| async move {
             let cwd = TERMINAL_DB
                 .get_working_directory(item_id, workspace_id)
@@ -762,7 +762,7 @@ impl Item for TerminalView {
                 });
 
             let terminal = project.update(&mut cx, |project, cx| {
-                project.create_terminal(cwd, window_id, cx)
+                project.create_terminal(cwd, window, cx)
             })?;
             Ok(pane.update(&mut cx, |_, cx| {
                 cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
@@ -1070,7 +1070,9 @@ mod tests {
         });
 
         let project = Project::test(params.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
 
         (project, workspace)
     }

crates/theme/src/theme.rs 🔗

@@ -402,6 +402,7 @@ pub struct StatusBar {
     pub height: f32,
     pub item_spacing: f32,
     pub cursor_position: TextStyle,
+    pub vim_mode_indicator: ContainedText,
     pub active_language: Interactive<ContainedText>,
     pub auto_update_progress_message: TextStyle,
     pub auto_update_done_message: TextStyle,

crates/theme/src/ui.rs 🔗

@@ -192,7 +192,6 @@ where
     F: FnOnce(&mut gpui::ViewContext<V>) -> D,
 {
     const TITLEBAR_HEIGHT: f32 = 28.;
-    // let active = cx.window_is_active(cx.window_id());
 
     Flex::column()
         .with_child(

crates/util/src/paths.rs 🔗

@@ -30,49 +30,47 @@ pub mod legacy {
     }
 }
 
-/// Compacts a given file path by replacing the user's home directory
-/// prefix with a tilde (`~`).
-///
-/// # Arguments
-///
-/// * `path` - A reference to a `Path` representing the file path to compact.
-///
-/// # Examples
-///
-/// ```
-/// use std::path::{Path, PathBuf};
-/// use util::paths::compact;
-/// let path: PathBuf = [
-///     util::paths::HOME.to_string_lossy().to_string(),
-///     "some_file.txt".to_string(),
-///  ]
-///  .iter()
-///  .collect();
-/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
-///     assert_eq!(compact(&path).to_str(), Some("~/some_file.txt"));
-/// } else {
-///     assert_eq!(compact(&path).to_str(), path.to_str());
-/// }
-/// ```
-///
-/// # Returns
-///
-/// * A `PathBuf` containing the compacted file path. If the input path
-///   does not have the user's home directory prefix, or if we are not on
-///   Linux or macOS, the original path is returned unchanged.
-pub fn compact(path: &Path) -> PathBuf {
-    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
-        match path.strip_prefix(HOME.as_path()) {
-            Ok(relative_path) => {
-                let mut shortened_path = PathBuf::new();
-                shortened_path.push("~");
-                shortened_path.push(relative_path);
-                shortened_path
+pub trait PathExt {
+    fn compact(&self) -> PathBuf;
+    fn icon_suffix(&self) -> Option<&str>;
+}
+
+impl<T: AsRef<Path>> PathExt for T {
+    /// Compacts a given file path by replacing the user's home directory
+    /// prefix with a tilde (`~`).
+    ///
+    /// # Returns
+    ///
+    /// * A `PathBuf` containing the compacted file path. If the input path
+    ///   does not have the user's home directory prefix, or if we are not on
+    ///   Linux or macOS, the original path is returned unchanged.
+    fn compact(&self) -> PathBuf {
+        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+            match self.as_ref().strip_prefix(HOME.as_path()) {
+                Ok(relative_path) => {
+                    let mut shortened_path = PathBuf::new();
+                    shortened_path.push("~");
+                    shortened_path.push(relative_path);
+                    shortened_path
+                }
+                Err(_) => self.as_ref().to_path_buf(),
             }
-            Err(_) => path.to_path_buf(),
+        } else {
+            self.as_ref().to_path_buf()
         }
-    } else {
-        path.to_path_buf()
+    }
+
+    fn icon_suffix(&self) -> Option<&str> {
+        let file_name = self.as_ref().file_name()?.to_str()?;
+
+        if file_name.starts_with(".") {
+            return file_name.strip_prefix(".");
+        }
+
+        self.as_ref()
+            .extension()
+            .map(|extension| extension.to_str())
+            .flatten()
     }
 }
 
@@ -279,4 +277,42 @@ mod tests {
             );
         }
     }
+
+    #[test]
+    fn test_path_compact() {
+        let path: PathBuf = [
+            HOME.to_string_lossy().to_string(),
+            "some_file.txt".to_string(),
+        ]
+        .iter()
+        .collect();
+        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+            assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
+        } else {
+            assert_eq!(path.compact().to_str(), path.to_str());
+        }
+    }
+
+    #[test]
+    fn test_icon_suffix() {
+        // No dots in name
+        let path = Path::new("/a/b/c/file_name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Single dot in name
+        let path = Path::new("/a/b/c/file.name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Multiple dots in name
+        let path = Path::new("/a/b/c/long.file.name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Hidden file, no extension
+        let path = Path::new("/a/b/c/.gitignore");
+        assert_eq!(path.icon_suffix(), Some("gitignore"));
+
+        // Hidden file, with extension
+        let path = Path::new("/a/b/c/.eslintrc.js");
+        assert_eq!(path.icon_suffix(), Some("eslintrc.js"));
+    }
 }

crates/vector_store/src/modal.rs 🔗

@@ -1,172 +0,0 @@
-use crate::{SearchResult, VectorStore};
-use editor::{scroll::autoscroll::Autoscroll, Editor};
-use gpui::{
-    actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext,
-    WeakViewHandle,
-};
-use picker::{Picker, PickerDelegate, PickerEvent};
-use project::{Project, ProjectPath};
-use std::{collections::HashMap, sync::Arc, time::Duration};
-use util::ResultExt;
-use workspace::Workspace;
-
-const MIN_QUERY_LEN: usize = 5;
-const EMBEDDING_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(500);
-
-actions!(semantic_search, [Toggle]);
-
-pub type SemanticSearch = Picker<SemanticSearchDelegate>;
-
-pub struct SemanticSearchDelegate {
-    workspace: WeakViewHandle<Workspace>,
-    project: ModelHandle<Project>,
-    vector_store: ModelHandle<VectorStore>,
-    selected_match_index: usize,
-    matches: Vec<SearchResult>,
-    history: HashMap<String, Vec<SearchResult>>,
-}
-
-impl SemanticSearchDelegate {
-    // This is currently searching on every keystroke,
-    // This is wildly overkill, and has the potential to get expensive
-    // We will need to update this to throttle searching
-    pub fn new(
-        workspace: WeakViewHandle<Workspace>,
-        project: ModelHandle<Project>,
-        vector_store: ModelHandle<VectorStore>,
-    ) -> Self {
-        Self {
-            workspace,
-            project,
-            vector_store,
-            selected_match_index: 0,
-            matches: vec![],
-            history: HashMap::new(),
-        }
-    }
-}
-
-impl PickerDelegate for SemanticSearchDelegate {
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search repository in natural language...".into()
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<SemanticSearch>) {
-        if let Some(search_result) = self.matches.get(self.selected_match_index) {
-            // Open Buffer
-            let search_result = search_result.clone();
-            let buffer = self.project.update(cx, |project, cx| {
-                project.open_buffer(
-                    ProjectPath {
-                        worktree_id: search_result.worktree_id,
-                        path: search_result.file_path.clone().into(),
-                    },
-                    cx,
-                )
-            });
-
-            let workspace = self.workspace.clone();
-            let position = search_result.clone().offset;
-            cx.spawn(|_, mut cx| async move {
-                let buffer = buffer.await?;
-                workspace.update(&mut cx, |workspace, cx| {
-                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
-                    editor.update(cx, |editor, cx| {
-                        editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                            s.select_ranges([position..position])
-                        });
-                    });
-                })?;
-                Ok::<_, anyhow::Error>(())
-            })
-            .detach_and_log_err(cx);
-            cx.emit(PickerEvent::Dismiss);
-        }
-    }
-
-    fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_match_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
-        self.selected_match_index = ix;
-    }
-
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
-        log::info!("Searching for {:?}...", query);
-        if query.len() < MIN_QUERY_LEN {
-            log::info!("Query below minimum length");
-            return Task::ready(());
-        }
-
-        let vector_store = self.vector_store.clone();
-        let project = self.project.clone();
-        cx.spawn(|this, mut cx| async move {
-            cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
-
-            let retrieved_cached = this.update(&mut cx, |this, _| {
-                let delegate = this.delegate_mut();
-                if delegate.history.contains_key(&query) {
-                    let historic_results = delegate.history.get(&query).unwrap().to_owned();
-                    delegate.matches = historic_results.clone();
-                    true
-                } else {
-                    false
-                }
-            });
-
-            if let Some(retrieved) = retrieved_cached.log_err() {
-                if !retrieved {
-                    let task = vector_store.update(&mut cx, |store, cx| {
-                        store.search(project.clone(), query.to_string(), 10, cx)
-                    });
-
-                    if let Some(results) = task.await.log_err() {
-                        log::info!("Not queried previously, searching...");
-                        this.update(&mut cx, |this, _| {
-                            let delegate = this.delegate_mut();
-                            delegate.matches = results.clone();
-                            delegate.history.insert(query, results);
-                        })
-                        .ok();
-                    }
-                } else {
-                    log::info!("Already queried, retrieved directly from cached history");
-                }
-            }
-        })
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        mouse_state: &mut MouseState,
-        selected: bool,
-        cx: &AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let theme = theme::current(cx);
-        let style = &theme.picker.item;
-        let current_style = style.in_state(selected).style_for(mouse_state);
-
-        let search_result = &self.matches[ix];
-
-        let path = search_result.file_path.to_string_lossy();
-        let name = search_result.name.clone();
-
-        Flex::column()
-            .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
-            .with_child(Label::new(
-                path.to_string(),
-                style.inactive_state().default.label.clone(),
-            ))
-            .contained()
-            .with_style(current_style.container)
-            .into_any()
-    }
-}

crates/vector_store/src/parsing.rs 🔗

@@ -1,115 +0,0 @@
-use std::{path::PathBuf, sync::Arc, time::SystemTime};
-
-use anyhow::{anyhow, Ok, Result};
-use project::Fs;
-use tree_sitter::{Parser, QueryCursor};
-
-use crate::PendingFile;
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct Document {
-    pub offset: usize,
-    pub name: String,
-    pub embedding: Vec<f32>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct ParsedFile {
-    pub path: PathBuf,
-    pub mtime: SystemTime,
-    pub documents: Vec<Document>,
-}
-
-const CODE_CONTEXT_TEMPLATE: &str =
-    "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
-
-pub struct CodeContextRetriever {
-    pub parser: Parser,
-    pub cursor: QueryCursor,
-    pub fs: Arc<dyn Fs>,
-}
-
-impl CodeContextRetriever {
-    pub async fn parse_file(
-        &mut self,
-        pending_file: PendingFile,
-    ) -> Result<(ParsedFile, Vec<String>)> {
-        let grammar = pending_file
-            .language
-            .grammar()
-            .ok_or_else(|| anyhow!("no grammar for language"))?;
-        let embedding_config = grammar
-            .embedding_config
-            .as_ref()
-            .ok_or_else(|| anyhow!("no embedding queries"))?;
-
-        let content = self.fs.load(&pending_file.absolute_path).await?;
-
-        self.parser.set_language(grammar.ts_language).unwrap();
-
-        let tree = self
-            .parser
-            .parse(&content, None)
-            .ok_or_else(|| anyhow!("parsing failed"))?;
-
-        let mut documents = Vec::new();
-        let mut context_spans = Vec::new();
-
-        // Iterate through query matches
-        for mat in self.cursor.matches(
-            &embedding_config.query,
-            tree.root_node(),
-            content.as_bytes(),
-        ) {
-            // log::info!("-----MATCH-----");
-
-            let mut name = Vec::new();
-            let mut item: Option<&str> = None;
-            let mut offset: Option<usize> = None;
-            for capture in mat.captures {
-                if capture.index == embedding_config.item_capture_ix {
-                    offset = Some(capture.node.byte_range().start);
-                    item = content.get(capture.node.byte_range());
-                } else if capture.index == embedding_config.name_capture_ix {
-                    if let Some(name_content) = content.get(capture.node.byte_range()) {
-                        name.push(name_content);
-                    }
-                }
-
-                if let Some(context_capture_ix) = embedding_config.context_capture_ix {
-                    if capture.index == context_capture_ix {
-                        if let Some(context) = content.get(capture.node.byte_range()) {
-                            name.push(context);
-                        }
-                    }
-                }
-            }
-
-            if item.is_some() && offset.is_some() && name.len() > 0 {
-                let context_span = CODE_CONTEXT_TEMPLATE
-                    .replace("<path>", pending_file.relative_path.to_str().unwrap())
-                    .replace("<language>", &pending_file.language.name().to_lowercase())
-                    .replace("<item>", item.unwrap());
-
-                // log::info!("Name:       {:?}", name);
-                // log::info!("Span:       {:?}", util::truncate(&context_span, 100));
-
-                context_spans.push(context_span);
-                documents.push(Document {
-                    name: name.join(" "),
-                    offset: offset.unwrap(),
-                    embedding: Vec::new(),
-                })
-            }
-        }
-
-        return Ok((
-            ParsedFile {
-                path: pending_file.relative_path,
-                mtime: pending_file.modified_time,
-                documents,
-            },
-            context_spans,
-        ));
-    }
-}

crates/vector_store/src/vector_store.rs 🔗

@@ -1,770 +0,0 @@
-mod db;
-mod embedding;
-mod modal;
-mod parsing;
-mod vector_store_settings;
-
-#[cfg(test)]
-mod vector_store_tests;
-
-use crate::vector_store_settings::VectorStoreSettings;
-use anyhow::{anyhow, Result};
-use db::VectorDatabase;
-use embedding::{EmbeddingProvider, OpenAIEmbeddings};
-use futures::{channel::oneshot, Future};
-use gpui::{
-    AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, ViewContext,
-    WeakModelHandle,
-};
-use language::{Language, LanguageRegistry};
-use modal::{SemanticSearch, SemanticSearchDelegate, Toggle};
-use parsing::{CodeContextRetriever, ParsedFile};
-use project::{Fs, PathChange, Project, ProjectEntryId, WorktreeId};
-use smol::channel;
-use std::{
-    collections::HashMap,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::{Duration, Instant, SystemTime},
-};
-use tree_sitter::{Parser, QueryCursor};
-use util::{
-    channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
-    http::HttpClient,
-    paths::EMBEDDINGS_DIR,
-    ResultExt,
-};
-use workspace::{Workspace, WorkspaceCreated};
-
-const VECTOR_STORE_VERSION: usize = 0;
-const EMBEDDINGS_BATCH_SIZE: usize = 150;
-
-pub fn init(
-    fs: Arc<dyn Fs>,
-    http_client: Arc<dyn HttpClient>,
-    language_registry: Arc<LanguageRegistry>,
-    cx: &mut AppContext,
-) {
-    settings::register::<VectorStoreSettings>(cx);
-
-    let db_file_path = EMBEDDINGS_DIR
-        .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
-        .join("embeddings_db");
-
-    SemanticSearch::init(cx);
-    cx.add_action(
-        |workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>| {
-            if cx.has_global::<ModelHandle<VectorStore>>() {
-                let vector_store = cx.global::<ModelHandle<VectorStore>>().clone();
-                workspace.toggle_modal(cx, |workspace, cx| {
-                    let project = workspace.project().clone();
-                    let workspace = cx.weak_handle();
-                    cx.add_view(|cx| {
-                        SemanticSearch::new(
-                            SemanticSearchDelegate::new(workspace, project, vector_store),
-                            cx,
-                        )
-                    })
-                });
-            }
-        },
-    );
-
-    if *RELEASE_CHANNEL == ReleaseChannel::Stable
-        || !settings::get::<VectorStoreSettings>(cx).enabled
-    {
-        return;
-    }
-
-    cx.spawn(move |mut cx| async move {
-        let vector_store = VectorStore::new(
-            fs,
-            db_file_path,
-            // Arc::new(embedding::DummyEmbeddings {}),
-            Arc::new(OpenAIEmbeddings {
-                client: http_client,
-                executor: cx.background(),
-            }),
-            language_registry,
-            cx.clone(),
-        )
-        .await?;
-
-        cx.update(|cx| {
-            cx.set_global(vector_store.clone());
-            cx.subscribe_global::<WorkspaceCreated, _>({
-                let vector_store = vector_store.clone();
-                move |event, cx| {
-                    let workspace = &event.0;
-                    if let Some(workspace) = workspace.upgrade(cx) {
-                        let project = workspace.read(cx).project().clone();
-                        if project.read(cx).is_local() {
-                            vector_store.update(cx, |store, cx| {
-                                store.add_project(project, cx).detach();
-                            });
-                        }
-                    }
-                }
-            })
-            .detach();
-        });
-
-        anyhow::Ok(())
-    })
-    .detach();
-}
-
-pub struct VectorStore {
-    fs: Arc<dyn Fs>,
-    database_url: Arc<PathBuf>,
-    embedding_provider: Arc<dyn EmbeddingProvider>,
-    language_registry: Arc<LanguageRegistry>,
-    db_update_tx: channel::Sender<DbOperation>,
-    parsing_files_tx: channel::Sender<PendingFile>,
-    _db_update_task: Task<()>,
-    _embed_batch_task: Task<()>,
-    _batch_files_task: Task<()>,
-    _parsing_files_tasks: Vec<Task<()>>,
-    projects: HashMap<WeakModelHandle<Project>, ProjectState>,
-}
-
-struct ProjectState {
-    worktree_db_ids: Vec<(WorktreeId, i64)>,
-    pending_files: HashMap<PathBuf, (PendingFile, SystemTime)>,
-    _subscription: gpui::Subscription,
-}
-
-impl ProjectState {
-    fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
-        self.worktree_db_ids
-            .iter()
-            .find_map(|(worktree_id, db_id)| {
-                if *worktree_id == id {
-                    Some(*db_id)
-                } else {
-                    None
-                }
-            })
-    }
-
-    fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
-        self.worktree_db_ids
-            .iter()
-            .find_map(|(worktree_id, db_id)| {
-                if *db_id == id {
-                    Some(*worktree_id)
-                } else {
-                    None
-                }
-            })
-    }
-
-    fn update_pending_files(&mut self, pending_file: PendingFile, indexing_time: SystemTime) {
-        // If Pending File Already Exists, Replace it with the new one
-        // but keep the old indexing time
-        if let Some(old_file) = self
-            .pending_files
-            .remove(&pending_file.relative_path.clone())
-        {
-            self.pending_files.insert(
-                pending_file.relative_path.clone(),
-                (pending_file, old_file.1),
-            );
-        } else {
-            self.pending_files.insert(
-                pending_file.relative_path.clone(),
-                (pending_file, indexing_time),
-            );
-        };
-    }
-
-    fn get_outstanding_files(&mut self) -> Vec<PendingFile> {
-        let mut outstanding_files = vec![];
-        let mut remove_keys = vec![];
-        for key in self.pending_files.keys().into_iter() {
-            if let Some(pending_details) = self.pending_files.get(key) {
-                let (pending_file, index_time) = pending_details;
-                if index_time <= &SystemTime::now() {
-                    outstanding_files.push(pending_file.clone());
-                    remove_keys.push(key.clone());
-                }
-            }
-        }
-
-        for key in remove_keys.iter() {
-            self.pending_files.remove(key);
-        }
-
-        return outstanding_files;
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct PendingFile {
-    worktree_db_id: i64,
-    relative_path: PathBuf,
-    absolute_path: PathBuf,
-    language: Arc<Language>,
-    modified_time: SystemTime,
-}
-
-#[derive(Debug, Clone)]
-pub struct SearchResult {
-    pub worktree_id: WorktreeId,
-    pub name: String,
-    pub offset: usize,
-    pub file_path: PathBuf,
-}
-
-enum DbOperation {
-    InsertFile {
-        worktree_id: i64,
-        indexed_file: ParsedFile,
-    },
-    Delete {
-        worktree_id: i64,
-        path: PathBuf,
-    },
-    FindOrCreateWorktree {
-        path: PathBuf,
-        sender: oneshot::Sender<Result<i64>>,
-    },
-    FileMTimes {
-        worktree_id: i64,
-        sender: oneshot::Sender<Result<HashMap<PathBuf, SystemTime>>>,
-    },
-}
-
-enum EmbeddingJob {
-    Enqueue {
-        worktree_id: i64,
-        parsed_file: ParsedFile,
-        document_spans: Vec<String>,
-    },
-    Flush,
-}
-
-impl VectorStore {
-    async fn new(
-        fs: Arc<dyn Fs>,
-        database_url: PathBuf,
-        embedding_provider: Arc<dyn EmbeddingProvider>,
-        language_registry: Arc<LanguageRegistry>,
-        mut cx: AsyncAppContext,
-    ) -> Result<ModelHandle<Self>> {
-        let database_url = Arc::new(database_url);
-
-        let db = cx
-            .background()
-            .spawn({
-                let fs = fs.clone();
-                let database_url = database_url.clone();
-                async move {
-                    if let Some(db_directory) = database_url.parent() {
-                        fs.create_dir(db_directory).await.log_err();
-                    }
-
-                    let db = VectorDatabase::new(database_url.to_string_lossy().to_string())?;
-                    anyhow::Ok(db)
-                }
-            })
-            .await?;
-
-        Ok(cx.add_model(|cx| {
-            // paths_tx -> embeddings_tx -> db_update_tx
-
-            //db_update_tx/rx: Updating Database
-            let (db_update_tx, db_update_rx) = channel::unbounded();
-            let _db_update_task = cx.background().spawn(async move {
-                while let Ok(job) = db_update_rx.recv().await {
-                    match job {
-                        DbOperation::InsertFile {
-                            worktree_id,
-                            indexed_file,
-                        } => {
-                            db.insert_file(worktree_id, indexed_file).log_err();
-                        }
-                        DbOperation::Delete { worktree_id, path } => {
-                            db.delete_file(worktree_id, path).log_err();
-                        }
-                        DbOperation::FindOrCreateWorktree { path, sender } => {
-                            let id = db.find_or_create_worktree(&path);
-                            sender.send(id).ok();
-                        }
-                        DbOperation::FileMTimes {
-                            worktree_id: worktree_db_id,
-                            sender,
-                        } => {
-                            let file_mtimes = db.get_file_mtimes(worktree_db_id);
-                            sender.send(file_mtimes).ok();
-                        }
-                    }
-                }
-            });
-
-            // embed_tx/rx: Embed Batch and Send to Database
-            let (embed_batch_tx, embed_batch_rx) =
-                channel::unbounded::<Vec<(i64, ParsedFile, Vec<String>)>>();
-            let _embed_batch_task = cx.background().spawn({
-                let db_update_tx = db_update_tx.clone();
-                let embedding_provider = embedding_provider.clone();
-                async move {
-                    while let Ok(mut embeddings_queue) = embed_batch_rx.recv().await {
-                        // Construct Batch
-                        let mut document_spans = vec![];
-                        for (_, _, document_span) in embeddings_queue.iter() {
-                            document_spans.extend(document_span.iter().map(|s| s.as_str()));
-                        }
-
-                        if let Ok(embeddings) = embedding_provider.embed_batch(document_spans).await
-                        {
-                            let mut i = 0;
-                            let mut j = 0;
-
-                            for embedding in embeddings.iter() {
-                                while embeddings_queue[i].1.documents.len() == j {
-                                    i += 1;
-                                    j = 0;
-                                }
-
-                                embeddings_queue[i].1.documents[j].embedding = embedding.to_owned();
-                                j += 1;
-                            }
-
-                            for (worktree_id, indexed_file, _) in embeddings_queue.into_iter() {
-                                for document in indexed_file.documents.iter() {
-                                    // TODO: Update this so it doesn't panic
-                                    assert!(
-                                        document.embedding.len() > 0,
-                                        "Document Embedding Not Complete"
-                                    );
-                                }
-
-                                db_update_tx
-                                    .send(DbOperation::InsertFile {
-                                        worktree_id,
-                                        indexed_file,
-                                    })
-                                    .await
-                                    .unwrap();
-                            }
-                        }
-                    }
-                }
-            });
-
-            // batch_tx/rx: Batch Files to Send for Embeddings
-            let (batch_files_tx, batch_files_rx) = channel::unbounded::<EmbeddingJob>();
-            let _batch_files_task = cx.background().spawn(async move {
-                let mut queue_len = 0;
-                let mut embeddings_queue = vec![];
-
-                while let Ok(job) = batch_files_rx.recv().await {
-                    let should_flush = match job {
-                        EmbeddingJob::Enqueue {
-                            document_spans,
-                            worktree_id,
-                            parsed_file,
-                        } => {
-                            queue_len += &document_spans.len();
-                            embeddings_queue.push((worktree_id, parsed_file, document_spans));
-                            queue_len >= EMBEDDINGS_BATCH_SIZE
-                        }
-                        EmbeddingJob::Flush => true,
-                    };
-
-                    if should_flush {
-                        embed_batch_tx.try_send(embeddings_queue).unwrap();
-                        embeddings_queue = vec![];
-                        queue_len = 0;
-                    }
-                }
-            });
-
-            // parsing_files_tx/rx: Parsing Files to Embeddable Documents
-            let (parsing_files_tx, parsing_files_rx) = channel::unbounded::<PendingFile>();
-
-            let mut _parsing_files_tasks = Vec::new();
-            // for _ in 0..cx.background().num_cpus() {
-            for _ in 0..1 {
-                let fs = fs.clone();
-                let parsing_files_rx = parsing_files_rx.clone();
-                let batch_files_tx = batch_files_tx.clone();
-                _parsing_files_tasks.push(cx.background().spawn(async move {
-                    let parser = Parser::new();
-                    let cursor = QueryCursor::new();
-                    let mut retriever = CodeContextRetriever { parser, cursor, fs };
-                    while let Ok(pending_file) = parsing_files_rx.recv().await {
-                        if let Some((indexed_file, document_spans)) =
-                            retriever.parse_file(pending_file.clone()).await.log_err()
-                        {
-                            batch_files_tx
-                                .try_send(EmbeddingJob::Enqueue {
-                                    worktree_id: pending_file.worktree_db_id,
-                                    parsed_file: indexed_file,
-                                    document_spans,
-                                })
-                                .unwrap();
-                        }
-
-                        if parsing_files_rx.len() == 0 {
-                            batch_files_tx.try_send(EmbeddingJob::Flush).unwrap();
-                        }
-                    }
-                }));
-            }
-
-            Self {
-                fs,
-                database_url,
-                embedding_provider,
-                language_registry,
-                db_update_tx,
-                parsing_files_tx,
-                _db_update_task,
-                _embed_batch_task,
-                _batch_files_task,
-                _parsing_files_tasks,
-                projects: HashMap::new(),
-            }
-        }))
-    }
-
-    fn find_or_create_worktree(&self, path: PathBuf) -> impl Future<Output = Result<i64>> {
-        let (tx, rx) = oneshot::channel();
-        self.db_update_tx
-            .try_send(DbOperation::FindOrCreateWorktree { path, sender: tx })
-            .unwrap();
-        async move { rx.await? }
-    }
-
-    fn get_file_mtimes(
-        &self,
-        worktree_id: i64,
-    ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
-        let (tx, rx) = oneshot::channel();
-        self.db_update_tx
-            .try_send(DbOperation::FileMTimes {
-                worktree_id,
-                sender: tx,
-            })
-            .unwrap();
-        async move { rx.await? }
-    }
-
-    fn add_project(
-        &mut self,
-        project: ModelHandle<Project>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<()>> {
-        let worktree_scans_complete = project
-            .read(cx)
-            .worktrees(cx)
-            .map(|worktree| {
-                let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
-                async move {
-                    scan_complete.await;
-                }
-            })
-            .collect::<Vec<_>>();
-        let worktree_db_ids = project
-            .read(cx)
-            .worktrees(cx)
-            .map(|worktree| {
-                self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf())
-            })
-            .collect::<Vec<_>>();
-
-        let fs = self.fs.clone();
-        let language_registry = self.language_registry.clone();
-        let database_url = self.database_url.clone();
-        let db_update_tx = self.db_update_tx.clone();
-        let parsing_files_tx = self.parsing_files_tx.clone();
-
-        cx.spawn(|this, mut cx| async move {
-            futures::future::join_all(worktree_scans_complete).await;
-
-            let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
-
-            if let Some(db_directory) = database_url.parent() {
-                fs.create_dir(db_directory).await.log_err();
-            }
-
-            let worktrees = project.read_with(&cx, |project, cx| {
-                project
-                    .worktrees(cx)
-                    .map(|worktree| worktree.read(cx).snapshot())
-                    .collect::<Vec<_>>()
-            });
-
-            let mut worktree_file_times = HashMap::new();
-            let mut db_ids_by_worktree_id = HashMap::new();
-            for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
-                let db_id = db_id?;
-                db_ids_by_worktree_id.insert(worktree.id(), db_id);
-                worktree_file_times.insert(
-                    worktree.id(),
-                    this.read_with(&cx, |this, _| this.get_file_mtimes(db_id))
-                        .await?,
-                );
-            }
-
-            cx.background()
-                .spawn({
-                    let db_ids_by_worktree_id = db_ids_by_worktree_id.clone();
-                    let db_update_tx = db_update_tx.clone();
-                    let language_registry = language_registry.clone();
-                    let parsing_files_tx = parsing_files_tx.clone();
-                    async move {
-                        let t0 = Instant::now();
-                        for worktree in worktrees.into_iter() {
-                            let mut file_mtimes =
-                                worktree_file_times.remove(&worktree.id()).unwrap();
-                            for file in worktree.files(false, 0) {
-                                let absolute_path = worktree.absolutize(&file.path);
-
-                                if let Ok(language) = language_registry
-                                    .language_for_file(&absolute_path, None)
-                                    .await
-                                {
-                                    if language
-                                        .grammar()
-                                        .and_then(|grammar| grammar.embedding_config.as_ref())
-                                        .is_none()
-                                    {
-                                        continue;
-                                    }
-
-                                    let path_buf = file.path.to_path_buf();
-                                    let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
-                                    let already_stored = stored_mtime
-                                        .map_or(false, |existing_mtime| {
-                                            existing_mtime == file.mtime
-                                        });
-
-                                    if !already_stored {
-                                        parsing_files_tx
-                                            .try_send(PendingFile {
-                                                worktree_db_id: db_ids_by_worktree_id
-                                                    [&worktree.id()],
-                                                relative_path: path_buf,
-                                                absolute_path,
-                                                language,
-                                                modified_time: file.mtime,
-                                            })
-                                            .unwrap();
-                                    }
-                                }
-                            }
-                            for file in file_mtimes.keys() {
-                                db_update_tx
-                                    .try_send(DbOperation::Delete {
-                                        worktree_id: db_ids_by_worktree_id[&worktree.id()],
-                                        path: file.to_owned(),
-                                    })
-                                    .unwrap();
-                            }
-                        }
-                        log::info!(
-                            "Parsing Worktree Completed in {:?}",
-                            t0.elapsed().as_millis()
-                        );
-                    }
-                })
-                .detach();
-
-            // let mut pending_files: Vec<(PathBuf, ((i64, PathBuf, Arc<Language>, SystemTime), SystemTime))> = vec![];
-            this.update(&mut cx, |this, cx| {
-                // The below is managing for updated on save
-                // Currently each time a file is saved, this code is run, and for all the files that were changed, if the current time is
-                // greater than the previous embedded time by the REINDEXING_DELAY variable, we will send the file off to be indexed.
-                let _subscription = cx.subscribe(&project, |this, project, event, cx| {
-                    if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event {
-                        this.project_entries_changed(project, changes.clone(), cx, worktree_id);
-                    }
-                });
-
-                this.projects.insert(
-                    project.downgrade(),
-                    ProjectState {
-                        pending_files: HashMap::new(),
-                        worktree_db_ids: db_ids_by_worktree_id.into_iter().collect(),
-                        _subscription,
-                    },
-                );
-            });
-
-            anyhow::Ok(())
-        })
-    }
-
-    pub fn search(
-        &mut self,
-        project: ModelHandle<Project>,
-        phrase: String,
-        limit: usize,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<SearchResult>>> {
-        let project_state = if let Some(state) = self.projects.get(&project.downgrade()) {
-            state
-        } else {
-            return Task::ready(Err(anyhow!("project not added")));
-        };
-
-        let worktree_db_ids = project
-            .read(cx)
-            .worktrees(cx)
-            .filter_map(|worktree| {
-                let worktree_id = worktree.read(cx).id();
-                project_state.db_id_for_worktree_id(worktree_id)
-            })
-            .collect::<Vec<_>>();
-
-        let embedding_provider = self.embedding_provider.clone();
-        let database_url = self.database_url.clone();
-        cx.spawn(|this, cx| async move {
-            let documents = cx
-                .background()
-                .spawn(async move {
-                    let database = VectorDatabase::new(database_url.to_string_lossy().into())?;
-
-                    let phrase_embedding = embedding_provider
-                        .embed_batch(vec![&phrase])
-                        .await?
-                        .into_iter()
-                        .next()
-                        .unwrap();
-
-                    database.top_k_search(&worktree_db_ids, &phrase_embedding, limit)
-                })
-                .await?;
-
-            this.read_with(&cx, |this, _| {
-                let project_state = if let Some(state) = this.projects.get(&project.downgrade()) {
-                    state
-                } else {
-                    return Err(anyhow!("project not added"));
-                };
-
-                Ok(documents
-                    .into_iter()
-                    .filter_map(|(worktree_db_id, file_path, offset, name)| {
-                        let worktree_id = project_state.worktree_id_for_db_id(worktree_db_id)?;
-                        Some(SearchResult {
-                            worktree_id,
-                            name,
-                            offset,
-                            file_path,
-                        })
-                    })
-                    .collect())
-            })
-        })
-    }
-
-    fn project_entries_changed(
-        &mut self,
-        project: ModelHandle<Project>,
-        changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
-        cx: &mut ModelContext<'_, VectorStore>,
-        worktree_id: &WorktreeId,
-    ) -> Option<()> {
-        let reindexing_delay = settings::get::<VectorStoreSettings>(cx).reindexing_delay_seconds;
-
-        let worktree = project
-            .read(cx)
-            .worktree_for_id(worktree_id.clone(), cx)?
-            .read(cx)
-            .snapshot();
-
-        let worktree_db_id = self
-            .projects
-            .get(&project.downgrade())?
-            .db_id_for_worktree_id(worktree.id())?;
-        let file_mtimes = self.get_file_mtimes(worktree_db_id);
-
-        let language_registry = self.language_registry.clone();
-
-        cx.spawn(|this, mut cx| async move {
-            let file_mtimes = file_mtimes.await.log_err()?;
-
-            for change in changes.into_iter() {
-                let change_path = change.0.clone();
-                let absolute_path = worktree.absolutize(&change_path);
-
-                // Skip if git ignored or symlink
-                if let Some(entry) = worktree.entry_for_id(change.1) {
-                    if entry.is_ignored || entry.is_symlink || entry.is_external {
-                        continue;
-                    }
-                }
-
-                match change.2 {
-                    PathChange::Removed => this.update(&mut cx, |this, _| {
-                        this.db_update_tx
-                            .try_send(DbOperation::Delete {
-                                worktree_id: worktree_db_id,
-                                path: absolute_path,
-                            })
-                            .unwrap();
-                    }),
-                    _ => {
-                        if let Ok(language) = language_registry
-                            .language_for_file(&change_path.to_path_buf(), None)
-                            .await
-                        {
-                            if language
-                                .grammar()
-                                .and_then(|grammar| grammar.embedding_config.as_ref())
-                                .is_none()
-                            {
-                                continue;
-                            }
-
-                            let modified_time =
-                                change_path.metadata().log_err()?.modified().log_err()?;
-
-                            let existing_time = file_mtimes.get(&change_path.to_path_buf());
-                            let already_stored = existing_time
-                                .map_or(false, |existing_time| &modified_time != existing_time);
-
-                            if !already_stored {
-                                this.update(&mut cx, |this, _| {
-                                    let reindex_time = modified_time
-                                        + Duration::from_secs(reindexing_delay as u64);
-
-                                    let project_state =
-                                        this.projects.get_mut(&project.downgrade())?;
-                                    project_state.update_pending_files(
-                                        PendingFile {
-                                            relative_path: change_path.to_path_buf(),
-                                            absolute_path,
-                                            modified_time,
-                                            worktree_db_id,
-                                            language: language.clone(),
-                                        },
-                                        reindex_time,
-                                    );
-
-                                    for file in project_state.get_outstanding_files() {
-                                        this.parsing_files_tx.try_send(file).unwrap();
-                                    }
-                                    Some(())
-                                });
-                            }
-                        }
-                    }
-                }
-            }
-
-            Some(())
-        })
-        .detach();
-
-        Some(())
-    }
-}
-
-impl Entity for VectorStore {
-    type Event = ();
-}

crates/vector_store/src/vector_store_tests.rs 🔗

@@ -1,161 +0,0 @@
-use crate::{
-    db::dot, embedding::EmbeddingProvider, vector_store_settings::VectorStoreSettings, VectorStore,
-};
-use anyhow::Result;
-use async_trait::async_trait;
-use gpui::{Task, TestAppContext};
-use language::{Language, LanguageConfig, LanguageRegistry};
-use project::{project_settings::ProjectSettings, FakeFs, Project};
-use rand::{rngs::StdRng, Rng};
-use serde_json::json;
-use settings::SettingsStore;
-use std::sync::Arc;
-use unindent::Unindent;
-
-#[gpui::test]
-async fn test_vector_store(cx: &mut TestAppContext) {
-    cx.update(|cx| {
-        cx.set_global(SettingsStore::test(cx));
-        settings::register::<VectorStoreSettings>(cx);
-        settings::register::<ProjectSettings>(cx);
-    });
-
-    let fs = FakeFs::new(cx.background());
-    fs.insert_tree(
-        "/the-root",
-        json!({
-            "src": {
-                "file1.rs": "
-                    fn aaa() {
-                        println!(\"aaaa!\");
-                    }
-
-                    fn zzzzzzzzz() {
-                        println!(\"SLEEPING\");
-                    }
-                ".unindent(),
-                "file2.rs": "
-                    fn bbb() {
-                        println!(\"bbbb!\");
-                    }
-                ".unindent(),
-            }
-        }),
-    )
-    .await;
-
-    let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
-    let rust_language = Arc::new(
-        Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".into()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        )
-        .with_embedding_query(
-            r#"
-            (function_item
-                name: (identifier) @name
-                body: (block)) @item
-            "#,
-        )
-        .unwrap(),
-    );
-    languages.add(rust_language);
-
-    let db_dir = tempdir::TempDir::new("vector-store").unwrap();
-    let db_path = db_dir.path().join("db.sqlite");
-
-    let store = VectorStore::new(
-        fs.clone(),
-        db_path,
-        Arc::new(FakeEmbeddingProvider),
-        languages,
-        cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    let project = Project::test(fs, ["/the-root".as_ref()], cx).await;
-    let worktree_id = project.read_with(cx, |project, cx| {
-        project.worktrees(cx).next().unwrap().read(cx).id()
-    });
-    store
-        .update(cx, |store, cx| store.add_project(project.clone(), cx))
-        .await
-        .unwrap();
-    cx.foreground().run_until_parked();
-
-    let search_results = store
-        .update(cx, |store, cx| {
-            store.search(project.clone(), "aaaa".to_string(), 5, cx)
-        })
-        .await
-        .unwrap();
-
-    assert_eq!(search_results[0].offset, 0);
-    assert_eq!(search_results[0].name, "aaa");
-    assert_eq!(search_results[0].worktree_id, worktree_id);
-}
-
-#[gpui::test]
-fn test_dot_product(mut rng: StdRng) {
-    assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.);
-    assert_eq!(dot(&[2., 0., 0., 0., 0.], &[3., 1., 0., 0., 0.]), 6.);
-
-    for _ in 0..100 {
-        let size = 1536;
-        let mut a = vec![0.; size];
-        let mut b = vec![0.; size];
-        for (a, b) in a.iter_mut().zip(b.iter_mut()) {
-            *a = rng.gen();
-            *b = rng.gen();
-        }
-
-        assert_eq!(
-            round_to_decimals(dot(&a, &b), 1),
-            round_to_decimals(reference_dot(&a, &b), 1)
-        );
-    }
-
-    fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
-        let factor = (10.0 as f32).powi(decimal_places);
-        (n * factor).round() / factor
-    }
-
-    fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
-        a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
-    }
-}
-
-struct FakeEmbeddingProvider;
-
-#[async_trait]
-impl EmbeddingProvider for FakeEmbeddingProvider {
-    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
-        Ok(spans
-            .iter()
-            .map(|span| {
-                let mut result = vec![1.0; 26];
-                for letter in span.chars() {
-                    let letter = letter.to_ascii_lowercase();
-                    if letter as u32 >= 'a' as u32 {
-                        let ix = (letter as u32) - ('a' as u32);
-                        if ix < 26 {
-                            result[ix as usize] += 1.0;
-                        }
-                    }
-                }
-
-                let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
-                for x in &mut result {
-                    *x /= norm;
-                }
-
-                result
-            })
-            .collect())
-    }
-}

crates/vim/Cargo.toml 🔗

@@ -32,6 +32,8 @@ language = { path = "../language" }
 search = { path = "../search" }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }
+theme = { path = "../theme" }
+language_selector = { path = "../language_selector"}
 
 [dev-dependencies]
 indoc.workspace = true
@@ -44,3 +46,4 @@ project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }

crates/vim/src/editor_events.rs 🔗

@@ -10,7 +10,7 @@ pub fn init(cx: &mut AppContext) {
 
 fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
     if let Some(previously_active_editor) = Vim::read(cx).active_editor.clone() {
-        cx.update_window(previously_active_editor.window_id(), |cx| {
+        previously_active_editor.window().update(cx, |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.update_active_editor(cx, |previously_active_editor, cx| {
                     vim.unhook_vim_settings(previously_active_editor, cx)
@@ -19,7 +19,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
         });
     }
 
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             vim.set_active_editor(editor.clone(), cx);
         });
@@ -27,7 +27,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
 }
 
 fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {
@@ -41,7 +41,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
 }
 
 fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         cx.update_default_global(|vim: &mut Vim, _| {
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {

crates/vim/src/mode_indicator.rs 🔗

@@ -0,0 +1,107 @@
+use gpui::{
+    elements::{Empty, Label},
+    AnyElement, Element, Entity, Subscription, View, ViewContext,
+};
+use settings::SettingsStore;
+use workspace::{item::ItemHandle, StatusItemView};
+
+use crate::{state::Mode, Vim, VimEvent, VimModeSetting};
+
+pub struct ModeIndicator {
+    pub mode: Option<Mode>,
+    _subscription: Subscription,
+}
+
+impl ModeIndicator {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let handle = cx.handle().downgrade();
+
+        let _subscription = cx.subscribe_global::<VimEvent, _>(move |&event, cx| {
+            if let Some(mode_indicator) = handle.upgrade(cx) {
+                match event {
+                    VimEvent::ModeChanged { mode } => {
+                        mode_indicator.window().update(cx, |cx| {
+                            mode_indicator.update(cx, move |mode_indicator, cx| {
+                                mode_indicator.set_mode(mode, cx);
+                            })
+                        });
+                    }
+                }
+            }
+        });
+
+        cx.observe_global::<SettingsStore, _>(move |mode_indicator, cx| {
+            if settings::get::<VimModeSetting>(cx).0 {
+                mode_indicator.mode = cx
+                    .has_global::<Vim>()
+                    .then(|| cx.global::<Vim>().state.mode);
+            } else {
+                mode_indicator.mode.take();
+            }
+        })
+        .detach();
+
+        // Vim doesn't exist in some tests
+        let mode = cx
+            .has_global::<Vim>()
+            .then(|| {
+                let vim = cx.global::<Vim>();
+                vim.enabled.then(|| vim.state.mode)
+            })
+            .flatten();
+
+        Self {
+            mode,
+            _subscription,
+        }
+    }
+
+    pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
+        if self.mode != Some(mode) {
+            self.mode = Some(mode);
+            cx.notify();
+        }
+    }
+}
+
+impl Entity for ModeIndicator {
+    type Event = ();
+}
+
+impl View for ModeIndicator {
+    fn ui_name() -> &'static str {
+        "ModeIndicatorView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let Some(mode) = self.mode.as_ref() else {
+            return Empty::new().into_any();
+        };
+
+        let theme = &theme::current(cx).workspace.status_bar;
+
+        // we always choose text to be 12 monospace characters
+        // so that as the mode indicator changes, the rest of the
+        // UI stays still.
+        let text = match mode {
+            Mode::Normal => "-- NORMAL --",
+            Mode::Insert => "-- INSERT --",
+            Mode::Visual { line: false } => "-- VISUAL --",
+            Mode::Visual { line: true } => "VISUAL LINE ",
+        };
+        Label::new(text, theme.vim_mode_indicator.text.clone())
+            .contained()
+            .with_style(theme.vim_mode_indicator.container)
+            .into_any()
+    }
+}
+
+impl StatusItemView for ModeIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        _active_pane_item: Option<&dyn ItemHandle>,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        // nothing to do.
+    }
+}

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

@@ -93,7 +93,7 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 search_bar.update(cx, |search_bar, cx| {
-                    let mut state = &mut vim.state.search;
+                    let state = &mut vim.state.search;
                     let mut count = state.count;
 
                     // in the case that the query has changed, the search bar
@@ -222,7 +222,7 @@ mod test {
         });
 
         search_bar.read_with(cx.cx, |bar, cx| {
-            assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+            assert_eq!(bar.query(cx), "cc");
         });
 
         deterministic.run_until_parked();

crates/vim/src/test.rs 🔗

@@ -4,6 +4,8 @@ mod neovim_connection;
 mod vim_binding_test_context;
 mod vim_test_context;
 
+use std::sync::Arc;
+
 use command_palette::CommandPalette;
 use editor::DisplayPoint;
 pub use neovim_backed_binding_test_context::*;
@@ -14,7 +16,7 @@ pub use vim_test_context::*;
 use indoc::indoc;
 use search::BufferSearchBar;
 
-use crate::state::Mode;
+use crate::{state::Mode, ModeIndicator};
 
 #[gpui::test]
 async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@@ -97,7 +99,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
     });
 
     search_bar.read_with(cx.cx, |bar, cx| {
-        assert_eq!(bar.query_editor.read(cx).text(cx), "");
+        assert_eq!(bar.query(cx), "");
     })
 }
 
@@ -173,7 +175,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
     });
 
     search_bar.read_with(cx.cx, |bar, cx| {
-        assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+        assert_eq!(bar.query(cx), "cc");
     });
 
     // wait for the query editor change event to fire.
@@ -195,3 +197,57 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
     cx.simulate_keystrokes(["shift-n"]);
     cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
 }
+
+#[gpui::test]
+async fn test_status_indicator(
+    cx: &mut gpui::TestAppContext,
+    deterministic: Arc<gpui::executor::Deterministic>,
+) {
+    let mut cx = VimTestContext::new(cx, true).await;
+    deterministic.run_until_parked();
+
+    let mode_indicator = cx.workspace(|workspace, cx| {
+        let status_bar = workspace.status_bar().read(cx);
+        let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
+        assert!(mode_indicator.is_some());
+        mode_indicator.unwrap()
+    });
+
+    assert_eq!(
+        cx.workspace(|_, cx| mode_indicator.read(cx).mode),
+        Some(Mode::Normal)
+    );
+
+    // shows the correct mode
+    cx.simulate_keystrokes(["i"]);
+    deterministic.run_until_parked();
+    assert_eq!(
+        cx.workspace(|_, cx| mode_indicator.read(cx).mode),
+        Some(Mode::Insert)
+    );
+
+    // shows even in search
+    cx.simulate_keystrokes(["escape", "v", "/"]);
+    deterministic.run_until_parked();
+    assert_eq!(
+        cx.workspace(|_, cx| mode_indicator.read(cx).mode),
+        Some(Mode::Visual { line: false })
+    );
+
+    // hides if vim mode is disabled
+    cx.disable_vim();
+    deterministic.run_until_parked();
+    cx.workspace(|workspace, cx| {
+        let status_bar = workspace.status_bar().read(cx);
+        let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
+        assert!(mode_indicator.read(cx).mode.is_none());
+    });
+
+    cx.enable_vim();
+    deterministic.run_until_parked();
+    cx.workspace(|workspace, cx| {
+        let status_bar = workspace.status_bar().read(cx);
+        let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
+        assert!(mode_indicator.read(cx).mode.is_some());
+    });
+}

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

@@ -43,6 +43,10 @@ impl<'a> VimTestContext<'a> {
                     toolbar.add_item(project_search_bar, cx);
                 })
             });
+            workspace.status_bar().update(cx, |status_bar, cx| {
+                let vim_mode_indicator = cx.add_view(ModeIndicator::new);
+                status_bar.add_right_item(vim_mode_indicator, cx);
+            });
         });
 
         Self { cx }
@@ -81,8 +85,8 @@ impl<'a> VimTestContext<'a> {
     }
 
     pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
-        let window_id = self.window_id;
-        self.update_window(window_id, |cx| {
+        let window = self.window;
+        window.update(self.cx.cx.cx, |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.switch_mode(mode, false, cx);
             })

crates/vim/src/vim.rs 🔗

@@ -3,6 +3,7 @@ mod test;
 
 mod editor_events;
 mod insert;
+mod mode_indicator;
 mod motion;
 mod normal;
 mod object;
@@ -18,6 +19,7 @@ use gpui::{
     Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use language::CursorShape;
+pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
@@ -41,6 +43,11 @@ struct Number(u8);
 actions!(vim, [Tab, Enter]);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
+#[derive(Copy, Clone, Debug)]
+enum VimEvent {
+    ModeChanged { mode: Mode },
+}
+
 pub fn init(cx: &mut AppContext) {
     settings::register::<VimModeSetting>(cx);
 
@@ -119,7 +126,6 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
 pub struct Vim {
     active_editor: Option<WeakViewHandle<Editor>>,
     editor_subscription: Option<Subscription>,
-
     enabled: bool,
     state: VimState,
 }
@@ -178,6 +184,8 @@ impl Vim {
         self.state.mode = mode;
         self.state.operator_stack.clear();
 
+        cx.emit_global(VimEvent::ModeChanged { mode });
+
         // Sync editor settings like clip mode
         self.sync_vim_settings(cx);
 

crates/workspace/src/dock.rs 🔗

@@ -203,7 +203,7 @@ impl Dock {
     pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
         self.panel_entries.iter().position(|entry| {
             let panel = entry.panel.as_any();
-            cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
+            cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
         })
     }
 
@@ -530,16 +530,15 @@ impl View for PanelButtons {
                                     tooltip_action.as_ref().map(|action| action.boxed_clone());
                                 move |_, this, cx| {
                                     if let Some(tooltip_action) = &tooltip_action {
-                                        let window_id = cx.window_id();
+                                        let window = cx.window();
                                         let view_id = this.workspace.id();
                                         let tooltip_action = tooltip_action.boxed_clone();
                                         cx.spawn(|_, mut cx| async move {
-                                            cx.dispatch_action(
-                                                window_id,
+                                            window.dispatch_action(
                                                 view_id,
                                                 &*tooltip_action,
-                                            )
-                                            .ok();
+                                                &mut cx,
+                                            );
                                         })
                                         .detach();
                                     }

crates/workspace/src/item.rs 🔗

@@ -6,6 +6,7 @@ use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
 use anyhow::Result;
 use client::{proto, Client};
 use gpui::geometry::vector::Vector2F;
+use gpui::AnyWindowHandle;
 use gpui::{
     fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@@ -250,7 +251,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn workspace_deactivated(&self, cx: &mut WindowContext);
     fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
     fn id(&self) -> usize;
-    fn window_id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
     fn as_any(&self) -> &AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
     fn has_conflict(&self, cx: &AppContext) -> bool;
@@ -280,7 +281,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
 
 pub trait WeakItemHandle {
     fn id(&self) -> usize;
-    fn window_id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 }
 
@@ -542,8 +543,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.id()
     }
 
-    fn window_id(&self) -> usize {
-        self.window_id()
+    fn window(&self) -> AnyWindowHandle {
+        AnyViewHandle::window(self)
     }
 
     fn as_any(&self) -> &AnyViewHandle {
@@ -649,8 +650,8 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
         self.id()
     }
 
-    fn window_id(&self) -> usize {
-        self.window_id()
+    fn window(&self) -> AnyWindowHandle {
+        self.window()
     }
 
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {

crates/workspace/src/pane.rs 🔗

@@ -746,6 +746,10 @@ impl Pane {
         _: &CloseAllItems,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+
         Some(self.close_items(cx, move |_| true))
     }
 
@@ -1913,8 +1917,8 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
             MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
                 gpui::platform::MouseButton::Left,
                 move |_, _: &mut V, cx| {
-                    let window_id = cx.window_id();
-                    cx.app_context().focus(window_id, Some(child_view_id))
+                    let window = cx.window();
+                    cx.app_context().focus(window, Some(child_view_id))
                 },
             ),
         );
@@ -1968,7 +1972,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         pane.update(cx, |pane, cx| {
@@ -1983,7 +1988,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -2061,7 +2067,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -2137,7 +2144,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view
@@ -2205,7 +2213,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", false, cx);
@@ -2252,7 +2261,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2272,7 +2282,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", true, cx);
@@ -2295,7 +2306,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2315,7 +2327,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2335,7 +2348,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", false, cx);

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -28,11 +28,11 @@ where
     let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
     let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
         drag_and_drop
-            .currently_dragged::<DraggedItem>(cx.window_id())
+            .currently_dragged::<DraggedItem>(cx.window())
             .map(|(drag_position, _)| drag_position)
             .or_else(|| {
                 drag_and_drop
-                    .currently_dragged::<ProjectEntryId>(cx.window_id())
+                    .currently_dragged::<ProjectEntryId>(cx.window())
                     .map(|(drag_position, _)| drag_position)
             })
     } else {
@@ -91,10 +91,10 @@ where
                 let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
 
                 if drag_and_drop
-                    .currently_dragged::<DraggedItem>(cx.window_id())
+                    .currently_dragged::<DraggedItem>(cx.window())
                     .is_some()
                     || drag_and_drop
-                        .currently_dragged::<ProjectEntryId>(cx.window_id())
+                        .currently_dragged::<ProjectEntryId>(cx.window())
                         .is_some()
                 {
                     cx.notify();
@@ -122,11 +122,11 @@ pub fn handle_dropped_item<V: View>(
     }
     let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
     let action = if let Some((_, dragged_item)) =
-        drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
+        drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
     {
         Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
     } else if let Some((_, project_entry)) =
-        drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
+        drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window())
     {
         Action::Open(*project_entry)
     } else {

crates/workspace/src/pane_group.rs 🔗

@@ -584,7 +584,7 @@ impl SplitDirection {
 }
 
 mod element {
-    use std::{cell::RefCell, ops::Range, rc::Rc};
+    use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
 
     use gpui::{
         geometry::{
@@ -593,8 +593,9 @@ mod element {
         },
         json::{self, ToJson},
         platform::{CursorStyle, MouseButton},
-        AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, PaintContext,
-        RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
+        scene::MouseDrag,
+        AnyElement, Axis, CursorRegion, Element, EventContext, LayoutContext, MouseRegion,
+        PaintContext, RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
     };
 
     use crate::{
@@ -682,6 +683,96 @@ mod element {
                 *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
             }
         }
+
+        fn handle_resize(
+            flexes: Rc<RefCell<Vec<f32>>>,
+            axis: Axis,
+            preceding_ix: usize,
+            child_start: Vector2F,
+            drag_bounds: RectF,
+        ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
+            let size = move |ix, flexes: &[f32]| {
+                drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
+            };
+
+            move |drag, workspace: &mut Workspace, cx| {
+                if drag.end {
+                    // TODO: Clear cascading resize state
+                    return;
+                }
+                let min_size = match axis {
+                    Axis::Horizontal => HORIZONTAL_MIN_SIZE,
+                    Axis::Vertical => VERTICAL_MIN_SIZE,
+                };
+                let mut flexes = flexes.borrow_mut();
+
+                // Don't allow resizing to less than the minimum size, if elements are already too small
+                if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
+                    return;
+                }
+
+                let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
+                    - size(preceding_ix, flexes.as_slice());
+
+                let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+                    let flex_change = pixel_dx / drag_bounds.length_along(axis);
+                    let current_target_flex = flexes[target_ix] + flex_change;
+                    let next_target_flex =
+                        flexes[(target_ix as isize + next) as usize] - flex_change;
+                    (current_target_flex, next_target_flex)
+                };
+
+                let mut successors = from_fn({
+                    let forward = proposed_current_pixel_change > 0.;
+                    let mut ix_offset = 0;
+                    let len = flexes.len();
+                    move || {
+                        let result = if forward {
+                            (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
+                        } else {
+                            (preceding_ix as isize - ix_offset as isize >= 0)
+                                .then(|| preceding_ix - ix_offset)
+                        };
+
+                        ix_offset += 1;
+
+                        result
+                    }
+                });
+
+                while proposed_current_pixel_change.abs() > 0. {
+                    let Some(current_ix) = successors.next() else {
+                            break;
+                        };
+
+                    let next_target_size = f32::max(
+                        size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
+                        min_size,
+                    );
+
+                    let current_target_size = f32::max(
+                        size(current_ix, flexes.as_slice())
+                            + size(current_ix + 1, flexes.as_slice())
+                            - next_target_size,
+                        min_size,
+                    );
+
+                    let current_pixel_change =
+                        current_target_size - size(current_ix, flexes.as_slice());
+
+                    let (current_target_flex, next_target_flex) =
+                        flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
+
+                    flexes[current_ix] = current_target_flex;
+                    flexes[current_ix + 1] = next_target_flex;
+
+                    proposed_current_pixel_change -= current_pixel_change;
+                }
+
+                workspace.schedule_serialize(cx);
+                cx.notify();
+            }
+        }
     }
 
     impl Extend<AnyElement<Workspace>> for PaneAxisElement {
@@ -792,8 +883,7 @@ mod element {
                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
                 }
 
-                if let Some(Some((next_ix, next_child))) = can_resize.then(|| children_iter.peek())
-                {
+                if can_resize && children_iter.peek().is_some() {
                     scene.push_stacking_context(None, None);
 
                     let handle_origin = match self.axis {
@@ -822,15 +912,6 @@ mod element {
                         style,
                     });
 
-                    let axis = self.axis;
-                    let child_size = child.size();
-                    let next_child_size = next_child.size();
-                    let drag_bounds = visible_bounds.clone();
-                    let flexes = self.flexes.borrow();
-                    let current_flex = flexes[ix];
-                    let next_ix = *next_ix;
-                    let next_flex = flexes[next_ix];
-                    drop(flexes);
                     enum ResizeHandle {}
                     let mut mouse_region = MouseRegion::new::<ResizeHandle>(
                         cx.view_id(),
@@ -838,56 +919,16 @@ mod element {
                         handle_bounds,
                     );
                     mouse_region = mouse_region
-                        .on_drag(MouseButton::Left, {
-                            let flexes = self.flexes.clone();
-                            move |drag, workspace: &mut Workspace, cx| {
-                                let min_size = match axis {
-                                    Axis::Horizontal => HORIZONTAL_MIN_SIZE,
-                                    Axis::Vertical => VERTICAL_MIN_SIZE,
-                                };
-                                // Don't allow resizing to less than the minimum size, if elements are already too small
-                                if min_size - 1. > child_size.along(axis)
-                                    || min_size - 1. > next_child_size.along(axis)
-                                {
-                                    return;
-                                }
-
-                                let mut current_target_size =
-                                    (drag.position - child_start).along(axis);
-
-                                let proposed_current_pixel_change =
-                                    current_target_size - child_size.along(axis);
-
-                                if proposed_current_pixel_change < 0. {
-                                    current_target_size = f32::max(current_target_size, min_size);
-                                } else if proposed_current_pixel_change > 0. {
-                                    // TODO: cascade this change to other children if current item is at min size
-                                    let next_target_size = f32::max(
-                                        next_child_size.along(axis) - proposed_current_pixel_change,
-                                        min_size,
-                                    );
-                                    current_target_size = f32::min(
-                                        current_target_size,
-                                        child_size.along(axis) + next_child_size.along(axis)
-                                            - next_target_size,
-                                    );
-                                }
-
-                                let current_pixel_change =
-                                    current_target_size - child_size.along(axis);
-                                let flex_change =
-                                    current_pixel_change / drag_bounds.length_along(axis);
-                                let current_target_flex = current_flex + flex_change;
-                                let next_target_flex = next_flex - flex_change;
-
-                                let mut borrow = flexes.borrow_mut();
-                                *borrow.get_mut(ix).unwrap() = current_target_flex;
-                                *borrow.get_mut(next_ix).unwrap() = next_target_flex;
-
-                                workspace.schedule_serialize(cx);
-                                cx.notify();
-                            }
-                        })
+                        .on_drag(
+                            MouseButton::Left,
+                            Self::handle_resize(
+                                self.flexes.clone(),
+                                self.axis,
+                                ix,
+                                child_start,
+                                visible_bounds.clone(),
+                            ),
+                        )
                         .on_click(MouseButton::Left, {
                             let flexes = self.flexes.clone();
                             move |e, v: &mut Workspace, cx| {

crates/workspace/src/searchable.rs 🔗

@@ -235,7 +235,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
 
 impl PartialEq for Box<dyn SearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id() && self.window_id() == other.window_id()
+        self.id() == other.id() && self.window() == other.window()
     }
 }
 
@@ -259,7 +259,7 @@ impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
 
 impl PartialEq for Box<dyn WeakSearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id() && self.window_id() == other.window_id()
+        self.id() == other.id() && self.window() == other.window()
     }
 }
 
@@ -267,6 +267,6 @@ impl Eq for Box<dyn WeakSearchableItemHandle> {}
 
 impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        (self.id(), self.window_id()).hash(state)
+        (self.id(), self.window().id()).hash(state)
     }
 }

crates/workspace/src/status_bar.rs 🔗

@@ -27,6 +27,7 @@ trait StatusItemViewHandle {
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut WindowContext,
     );
+    fn ui_name(&self) -> &'static str;
 }
 
 pub struct StatusBar {
@@ -57,7 +58,6 @@ impl View for StatusBar {
                         .with_margin_right(theme.item_spacing)
                 }))
                 .into_any(),
-
             right: Flex::row()
                 .with_children(self.right_items.iter().rev().map(|i| {
                     ChildView::new(i.as_any(), cx)
@@ -96,6 +96,56 @@ impl StatusBar {
         cx.notify();
     }
 
+    pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
+        self.left_items
+            .iter()
+            .chain(self.right_items.iter())
+            .find_map(|item| item.as_any().clone().downcast())
+    }
+
+    pub fn position_of_item<T>(&self) -> Option<usize>
+    where
+        T: StatusItemView,
+    {
+        for (index, item) in self.left_items.iter().enumerate() {
+            if item.as_ref().ui_name() == T::ui_name() {
+                return Some(index);
+            }
+        }
+        for (index, item) in self.right_items.iter().enumerate() {
+            if item.as_ref().ui_name() == T::ui_name() {
+                return Some(index + self.left_items.len());
+            }
+        }
+        return None;
+    }
+
+    pub fn insert_item_after<T>(
+        &mut self,
+        position: usize,
+        item: ViewHandle<T>,
+        cx: &mut ViewContext<Self>,
+    ) where
+        T: 'static + StatusItemView,
+    {
+        if position < self.left_items.len() {
+            self.left_items.insert(position + 1, Box::new(item))
+        } else {
+            self.right_items
+                .insert(position + 1 - self.left_items.len(), Box::new(item))
+        }
+        cx.notify()
+    }
+
+    pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
+        if position < self.left_items.len() {
+            self.left_items.remove(position);
+        } else {
+            self.right_items.remove(position - self.left_items.len());
+        }
+        cx.notify();
+    }
+
     pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
     where
         T: 'static + StatusItemView,
@@ -133,6 +183,10 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
             this.set_active_pane_item(active_pane_item, cx)
         });
     }
+
+    fn ui_name(&self) -> &'static str {
+        T::ui_name()
+    }
 }
 
 impl From<&dyn StatusItemViewHandle> for AnyViewHandle {

crates/workspace/src/workspace.rs 🔗

@@ -37,7 +37,7 @@ use gpui::{
     },
     AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
     ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    WeakViewHandle, WindowContext, WindowHandle,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use itertools::Itertools;
@@ -122,6 +122,7 @@ actions!(
         NewFile,
         NewWindow,
         CloseWindow,
+        CloseInactiveTabsAndPanes,
         AddFolderToProject,
         Unfollow,
         Save,
@@ -240,6 +241,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_global_action(Workspace::close_global);
     cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
@@ -747,7 +749,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
-        requesting_window_id: Option<usize>,
+        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<(
         WeakViewHandle<Workspace>,
@@ -791,20 +793,13 @@ impl Workspace {
                 DB.next_id().await.unwrap_or(0)
             };
 
-            let workspace = requesting_window_id
-                .and_then(|window_id| {
-                    cx.update(|cx| {
-                        cx.replace_root_view(window_id, |cx| {
-                            Workspace::new(
-                                workspace_id,
-                                project_handle.clone(),
-                                app_state.clone(),
-                                cx,
-                            )
-                        })
-                    })
-                })
-                .unwrap_or_else(|| {
+            let window = if let Some(window) = requesting_window {
+                window.replace_root(&mut cx, |cx| {
+                    Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+                });
+                window
+            } else {
+                {
                     let window_bounds_override = window_bounds_env_override(&cx);
                     let (bounds, display) = if let Some(bounds) = window_bounds_override {
                         (Some(bounds), None)
@@ -850,8 +845,12 @@ impl Workspace {
                             )
                         },
                     )
-                    .1
-                });
+                }
+            };
+
+            // We haven't yielded the main thread since obtaining the window handle,
+            // so the window exists.
+            let workspace = window.root(&cx).unwrap();
 
             (app_state.initialize_workspace)(
                 workspace.downgrade(),
@@ -862,7 +861,7 @@ impl Workspace {
             .await
             .log_err();
 
-            cx.update_window(workspace.window_id(), |cx| cx.activate_window());
+            window.update(&mut cx, |cx| cx.activate_window());
 
             let workspace = workspace.downgrade();
             notify_if_database_failed(&workspace, &mut cx);
@@ -1233,14 +1232,14 @@ impl Workspace {
 
     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
         cx.spawn(|mut cx| async move {
-            let id = cx
-                .window_ids()
+            let window = cx
+                .windows()
                 .into_iter()
-                .find(|&id| cx.window_is_active(id));
-            if let Some(id) = id {
+                .find(|window| window.is_active(&cx).unwrap_or(false));
+            if let Some(window) = window {
                 //This can only get called when the window's project connection has been lost
                 //so we don't need to prompt the user for anything and instead just close the window
-                cx.remove_window(id);
+                window.remove(&mut cx);
             }
         })
         .detach();
@@ -1251,11 +1250,11 @@ impl Workspace {
         _: &CloseWindow,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
-        let window_id = cx.window_id();
+        let window = cx.window();
         let prepare = self.prepare_to_close(false, cx);
         Some(cx.spawn(|_, mut cx| async move {
             if prepare.await? {
-                cx.remove_window(window_id);
+                window.remove(&mut cx);
             }
             Ok(())
         }))
@@ -1267,13 +1266,13 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         let active_call = self.active_call().cloned();
-        let window_id = cx.window_id();
+        let window = cx.window();
 
         cx.spawn(|this, mut cx| async move {
             let workspace_count = cx
-                .window_ids()
+                .windows()
                 .into_iter()
-                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+                .filter(|window| window.root_is::<Workspace>())
                 .count();
 
             if let Some(active_call) = active_call {
@@ -1281,11 +1280,11 @@ impl Workspace {
                     && workspace_count == 1
                     && active_call.read_with(&cx, |call, _| call.room().is_some())
                 {
-                    let answer = cx.prompt(
-                        window_id,
+                    let answer = window.prompt(
                         PromptLevel::Warning,
                         "Do you want to leave the current call?",
                         &["Close window and hang up", "Cancel"],
+                        &mut cx,
                     );
 
                     if let Some(mut answer) = answer {
@@ -1391,7 +1390,7 @@ impl Workspace {
         paths: Vec<PathBuf>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        let window_id = cx.window_id();
+        let window = cx.window().downcast::<Self>();
         let is_remote = self.project.read(cx).is_remote();
         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
@@ -1403,15 +1402,15 @@ impl Workspace {
         let app_state = self.app_state.clone();
 
         cx.spawn(|_, mut cx| async move {
-            let window_id_to_replace = if let Some(close_task) = close_task {
+            let window_to_replace = if let Some(close_task) = close_task {
                 if !close_task.await? {
                     return Ok(());
                 }
-                Some(window_id)
+                window
             } else {
                 None
             };
-            cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
+            cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
                 .await?;
             Ok(())
         })
@@ -1671,6 +1670,45 @@ impl Workspace {
         }
     }
 
+    pub fn close_inactive_items_and_panes(
+        &mut self,
+        _: &CloseInactiveTabsAndPanes,
+        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);
+        };
+
+        for pane in self.panes() {
+            if 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)
+            }) {
+                tasks.push(close_pane_items)
+            }
+        }
+
+        if tasks.is_empty() {
+            None
+        } else {
+            Some(cx.spawn(|_, _| async move {
+                for task in tasks {
+                    task.await?
+                }
+                Ok(())
+            }))
+        }
+    }
+
     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
         let dock = match dock_side {
             DockPosition::Left => &self.left_dock,
@@ -3143,7 +3181,7 @@ impl Workspace {
             let left_visible = left_dock.is_open();
             let left_active_panel = left_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3156,7 +3194,7 @@ impl Workspace {
             let right_visible = right_dock.is_open();
             let right_active_panel = right_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3169,7 +3207,7 @@ impl Workspace {
             let bottom_visible = bottom_dock.is_open();
             let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3789,9 +3827,9 @@ pub fn activate_workspace_for_project(
     cx: &mut AsyncAppContext,
     predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
 ) -> Option<WeakViewHandle<Workspace>> {
-    for window_id in cx.window_ids() {
-        let handle = cx
-            .update_window(window_id, |cx| {
+    for window in cx.windows() {
+        let handle = window
+            .update(cx, |cx| {
                 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
                     let project = workspace_handle.read(cx).project.clone();
                     if project.update(cx, &predicate) {
@@ -3818,7 +3856,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
-    requesting_window_id: Option<usize>,
+    requesting_window: Option<WindowHandle<Workspace>>,
     cx: &mut AppContext,
 ) -> Task<
     Result<(
@@ -3846,7 +3884,7 @@ pub fn open_paths(
         } else {
             Ok(cx
                 .update(|cx| {
-                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
+                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
                 })
                 .await)
         }
@@ -3907,18 +3945,23 @@ pub fn join_remote_project(
 ) -> Task<Result<()>> {
     cx.spawn(|mut cx| async move {
         let existing_workspace = cx
-            .window_ids()
+            .windows()
             .into_iter()
-            .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
-            .find(|workspace| {
-                cx.read_window(workspace.window_id(), |cx| {
-                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
+            .find_map(|window| {
+                window.downcast::<Workspace>().and_then(|window| {
+                    window.read_root_with(&cx, |workspace, cx| {
+                        if workspace.project().read(cx).remote_id() == Some(project_id) {
+                            Some(cx.handle().downgrade())
+                        } else {
+                            None
+                        }
+                    })
                 })
-                .unwrap_or(false)
-            });
+            })
+            .flatten();
 
         let workspace = if let Some(existing_workspace) = existing_workspace {
-            existing_workspace.downgrade()
+            existing_workspace
         } else {
             let active_call = cx.read(ActiveCall::global);
             let room = active_call
@@ -3936,7 +3979,7 @@ pub fn join_remote_project(
                 .await?;
 
             let window_bounds_override = window_bounds_env_override(&cx);
-            let (_, workspace) = cx.add_window(
+            let window = cx.add_window(
                 (app_state.build_window_options)(
                     window_bounds_override,
                     None,
@@ -3944,6 +3987,7 @@ pub fn join_remote_project(
                 ),
                 |cx| Workspace::new(0, project, app_state.clone(), cx),
             );
+            let workspace = window.root(&cx).unwrap();
             (app_state.initialize_workspace)(
                 workspace.downgrade(),
                 false,
@@ -3956,7 +4000,7 @@ pub fn join_remote_project(
             workspace.downgrade()
         };
 
-        cx.activate_window(workspace.window_id());
+        workspace.window().activate(&mut cx);
         cx.platform().activate(true);
 
         workspace.update(&mut cx, |workspace, cx| {
@@ -3995,29 +4039,22 @@ pub fn join_remote_project(
 pub fn restart(_: &Restart, cx: &mut AppContext) {
     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
-        let mut workspaces = cx
-            .window_ids()
+        let mut workspace_windows = cx
+            .windows()
             .into_iter()
-            .filter_map(|window_id| {
-                Some(
-                    cx.root_view(window_id)?
-                        .clone()
-                        .downcast::<Workspace>()?
-                        .downgrade(),
-                )
-            })
+            .filter_map(|window| window.downcast::<Workspace>())
             .collect::<Vec<_>>();
 
         // If multiple windows have unsaved changes, and need a save prompt,
         // prompt in the active window before switching to a different window.
-        workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
+        workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
 
-        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
-            let answer = cx.prompt(
-                workspace.window_id(),
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
+            let answer = window.prompt(
                 PromptLevel::Info,
                 "Are you sure you want to restart?",
                 &["Restart", "Cancel"],
+                &mut cx,
             );
 
             if let Some(mut answer) = answer {
@@ -4029,14 +4066,13 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
         }
 
         // If the user cancels any save prompt, then keep the app open.
-        for workspace in workspaces {
-            if !workspace
-                .update(&mut cx, |workspace, cx| {
-                    workspace.prepare_to_close(true, cx)
-                })?
-                .await?
-            {
-                return Ok(());
+        for window in workspace_windows {
+            if let Some(close) = window.update_root(&mut cx, |workspace, cx| {
+                workspace.prepare_to_close(true, cx)
+            }) {
+                if !close.await? {
+                    return Ok(());
+                }
             }
         }
         cx.platform().restart();
@@ -4072,10 +4108,11 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Adding an item with no ambiguity renders the tab without detail.
-        let item1 = cx.add_view(window_id, |_| {
+        let item1 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
             item
@@ -4087,7 +4124,7 @@ mod tests {
 
         // Adding an item that creates ambiguity increases the level of detail on
         // both tabs.
-        let item2 = cx.add_view(window_id, |_| {
+        let item2 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
             item
@@ -4101,7 +4138,7 @@ mod tests {
         // Adding an item that creates ambiguity increases the level of detail only
         // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
         // we stop at the highest detail available.
-        let item3 = cx.add_view(window_id, |_| {
+        let item3 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
             item
@@ -4136,16 +4173,17 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
 
-        let item1 = cx.add_view(window_id, |cx| {
+        let item1 = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
         });
-        let item2 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
         });
 
@@ -4159,17 +4197,11 @@ mod tests {
                     .map(|e| e.id)
             );
         });
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
 
         // Add a second item to a non-empty pane
         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("two.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1"));
         project.read_with(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
@@ -4185,10 +4217,7 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
         project.read_with(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
@@ -4206,16 +4235,13 @@ mod tests {
             .await
             .unwrap();
         assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
+            window.current_title(cx).as_deref(),
             Some("one.txt — root1, root2")
         );
 
         // Remove a project folder
         project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root2")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2"));
     }
 
     #[gpui::test]
@@ -4226,18 +4252,19 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // When there are no dirty items, there's nothing to do.
-        let item1 = cx.add_view(window_id, |_| TestItem::new());
+        let item1 = window.add_view(cx, |_| TestItem::new());
         workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         assert!(task.await.unwrap());
 
         // When there are dirty untitled items, prompt to save each one. If the user
         // cancels any prompt, then abort.
-        let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
-        let item3 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
+        let item3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@@ -4248,9 +4275,9 @@ mod tests {
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         cx.foreground().run_until_parked();
-        cx.simulate_prompt_answer(window_id, 2 /* cancel */);
+        window.simulate_prompt_answer(2, cx); // cancel
         cx.foreground().run_until_parked();
-        assert!(!cx.has_pending_prompt(window_id));
+        assert!(!window.has_pending_prompt(cx));
         assert!(!task.await.unwrap());
     }
 
@@ -4261,26 +4288,27 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
-        let item1 = cx.add_view(window_id, |cx| {
+        let item1 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
-        let item2 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_conflict(true)
                 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
         });
-        let item3 = cx.add_view(window_id, |cx| {
+        let item3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_conflict(true)
                 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
         });
-        let item4 = cx.add_view(window_id, |cx| {
+        let item4 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new_untitled(cx)])
@@ -4308,10 +4336,10 @@ mod tests {
             assert_eq!(pane.items_len(), 4);
             assert_eq!(pane.active_item().unwrap().id(), item1.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Confirm saving item 1.
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         cx.foreground().run_until_parked();
 
         // Item 1 is saved. There's a prompt to save item 3.
@@ -4322,10 +4350,10 @@ mod tests {
             assert_eq!(pane.items_len(), 3);
             assert_eq!(pane.active_item().unwrap().id(), item3.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Cancel saving item 3.
-        cx.simulate_prompt_answer(window_id, 1);
+        window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
 
         // Item 3 is reloaded. There's a prompt to save item 4.
@@ -4336,10 +4364,10 @@ mod tests {
             assert_eq!(pane.items_len(), 2);
             assert_eq!(pane.active_item().unwrap().id(), item4.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Confirm saving item 4.
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         cx.foreground().run_until_parked();
 
         // There's a prompt for a path for item 4.
@@ -4363,13 +4391,14 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
         let single_entry_items = (0..=4)
             .map(|project_entry_id| {
-                cx.add_view(window_id, |cx| {
+                window.add_view(cx, |cx| {
                     TestItem::new()
                         .with_dirty(true)
                         .with_project_items(&[TestProjectItem::new(
@@ -4380,7 +4409,7 @@ mod tests {
                 })
             })
             .collect::<Vec<_>>();
-        let item_2_3 = cx.add_view(window_id, |cx| {
+        let item_2_3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_singleton(false)
@@ -4389,7 +4418,7 @@ mod tests {
                     single_entry_items[3].read(cx).project_items[0].clone(),
                 ])
         });
-        let item_3_4 = cx.add_view(window_id, |cx| {
+        let item_3_4 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_singleton(false)
@@ -4441,7 +4470,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(0)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
 
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
@@ -4450,7 +4479,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(2)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
 
         cx.foreground().run_until_parked();
         close.await.unwrap();
@@ -4466,10 +4495,11 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
-        let item = cx.add_view(window_id, |cx| {
+        let item = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
         let item_id = item.id();
@@ -4488,7 +4518,7 @@ mod tests {
         });
 
         // Deactivating the window saves the file.
-        cx.simulate_window_activation(None);
+        window.simulate_deactivation(cx);
         deterministic.run_until_parked();
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
 
@@ -4509,12 +4539,12 @@ mod tests {
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
 
         // Deactivating the window still saves the file.
-        cx.simulate_window_activation(Some(window_id));
+        window.simulate_activation(cx);
         item.update(cx, |item, cx| {
             cx.focus_self();
             item.is_dirty = true;
         });
-        cx.simulate_window_activation(None);
+        window.simulate_deactivation(cx);
 
         deterministic.run_until_parked();
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
@@ -4551,7 +4581,7 @@ mod tests {
         pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
             .await
             .unwrap();
-        assert!(!cx.has_pending_prompt(window_id));
+        assert!(!window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
         // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
@@ -4572,7 +4602,7 @@ mod tests {
         let _close_items =
             pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
         deterministic.run_until_parked();
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
     }
 
@@ -4583,9 +4613,10 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
-        let item = cx.add_view(window_id, |cx| {
+        let item = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
@@ -4636,7 +4667,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let panel = workspace.update(cx, |workspace, cx| {
             let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
@@ -4783,7 +4815,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
             // Add panel_1 on the left, panel_2 on the right.
@@ -4938,7 +4971,7 @@ mod tests {
 
         // If focus is transferred to another view that's not a panel or another pane, we still show
         // the panel as zoomed.
-        let focus_receiver = cx.add_view(window_id, |_| EmptyView);
+        let focus_receiver = window.add_view(cx, |_| EmptyView);
         focus_receiver.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));

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.97.0"
+version = "0.99.0"
 publish = false
 
 [lib]
@@ -64,7 +64,7 @@ terminal_view = { path = "../terminal_view" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
-vector_store = { path = "../vector_store" }
+semantic_index = { path = "../semantic_index" }
 vim = { path = "../vim" }
 workspace = { path = "../workspace" }
 welcome = { path = "../welcome" }
@@ -128,6 +128,7 @@ tree-sitter-svelte.workspace = true
 tree-sitter-racket.workspace = true
 tree-sitter-yaml.workspace = true
 tree-sitter-lua.workspace = true
+tree-sitter-nix.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed/src/languages.rs 🔗

@@ -152,8 +152,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
         tree_sitter_php::language(),
         vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))],
     );
+
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
+    language("nix", tree_sitter_nix::language(), vec![]);
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed/src/languages/bash/config.toml 🔗

@@ -1,5 +1,6 @@
 name = "Shell Script"
-path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"]
+path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin"]
+line_comment = "# "
 first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },

crates/zed/src/languages/c/embedding.scm 🔗

@@ -0,0 +1,43 @@
+(
+    (comment)* @context
+    .
+    (declaration
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (function_definition
+        declarator: [
+            (function_declarator
+                declarator: (_) @name
+                )
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    ))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )

crates/zed/src/languages/cpp/embedding.scm 🔗

@@ -0,0 +1,61 @@
+(
+    (comment)* @context
+    .
+    (function_definition
+        (type_qualifier)? @name
+        type: (_)? @name
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name)))
+            (reference_declarator
+                ["&" "&&"] @name
+                (function_declarator
+                declarator: (_) @name))
+        ]
+        (type_qualifier)? @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (template_declaration
+        (class_specifier
+            "class" @name
+            name: (_) @name)
+            ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (class_specifier
+        "class" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (enum_specifier
+        "enum" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (declaration
+        type: (struct_specifier
+        "struct" @name)
+        declarator: (_) @name) @item
+)

crates/zed/src/languages/elixir/embedding.scm 🔗

@@ -0,0 +1,27 @@
+(
+    (unary_operator
+        operator: "@"
+        operand: (call
+            target: (identifier) @unary
+            (#match? @unary "^(doc)$"))
+        ) @context
+    .
+    (call
+        target: (identifier) @name
+        (arguments
+            [
+            (identifier) @name
+            (call
+                target: (identifier) @name)
+                (binary_operator
+                    left: (call
+                    target: (identifier) @name)
+                    operator: "when")
+            ])
+        (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+        )
+
+    (call
+        target: (identifier) @name
+        (arguments (alias) @name)
+        (#match? @name "^(defmodule|defprotocol)$")) @item

crates/zed/src/languages/go/embedding.scm 🔗

@@ -0,0 +1,24 @@
+(
+    (comment)* @context
+    .
+    (type_declaration
+        (type_spec
+            name: (_) @name)
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (function_declaration
+        name: (_) @name
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_declaration
+        name: (_) @name
+    ) @item
+)

crates/zed/src/languages/javascript/embedding.scm 🔗

@@ -1,56 +1,71 @@
-; (internal_module
-;     "namespace" @context
-;     name: (_) @name) @item
-
-(enum_declaration
-    "enum" @context
-    name: (_) @name) @item
-
-(function_declaration
-    "async"? @context
-    "function" @context
-    name: (_) @name) @item
-
-(interface_declaration
-    "interface" @context
-    name: (_) @name) @item
-
-; (program
-;     (export_statement
-;         (lexical_declaration
-;             ["let" "const"] @context
-;             (variable_declarator
-;                 name: (_) @name) @item)))
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
 
-(program
-    (lexical_declaration
-        ["let" "const"] @context
-        (variable_declarator
-            name: (_) @name) @item))
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
 
-(class_declaration
-    "class" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
 
-(method_definition
+(
+    (comment)* @context
+    .
     [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-        ]* @context
-    name: (_) @name) @item
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
 
-; (public_field_definition
-;     [
-;         "declare"
-;         "readonly"
-;         "abstract"
-;         "static"
-;         (accessibility_modifier)
-;         ]* @context
-;     name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

crates/zed/src/languages/json/embedding.scm 🔗

@@ -0,0 +1,14 @@
+; Only produce one embedding for the entire file.
+(document) @item
+
+; Collapse arrays, except for the first object.
+(array
+  "[" @keep
+  .
+  (object)? @keep
+  "]" @keep) @collapse
+
+; Collapse string values (but not keys).
+(pair value: (string
+  "\"" @keep
+  "\"" @keep) @collapse)

crates/zed/src/languages/lua/config.toml 🔗

@@ -7,3 +7,4 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+collapsed_placeholder = "--[ ... ]--"

crates/zed/src/languages/nix/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Nix"
+path_suffixes = ["nix"]
+line_comment = "# "
+block_comment = ["/* ", " */"]
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = true, newline = true },
+]

crates/zed/src/languages/nix/highlights.scm 🔗

@@ -0,0 +1,95 @@
+(comment) @comment
+
+[
+  "if"
+  "then"
+  "else"
+  "let"
+  "inherit"
+  "in"
+  "rec"
+  "with"
+  "assert"
+  "or"
+] @keyword
+
+[
+ (string_expression)
+ (indented_string_expression)
+] @string
+
+[
+  (path_expression)
+  (hpath_expression)
+  (spath_expression)
+] @string.special.path
+
+(uri_expression) @link_uri
+
+[
+  (integer_expression)
+  (float_expression)
+] @number
+
+(interpolation
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(escape_sequence) @escape
+(dollar_escape) @escape
+
+(function_expression
+  universal: (identifier) @parameter
+)
+
+(formal
+  name: (identifier) @parameter
+  "?"? @punctuation.delimiter)
+
+(select_expression
+  attrpath: (attrpath (identifier)) @property)
+
+(apply_expression
+  function: [
+    (variable_expression (identifier)) @function
+    (select_expression
+      attrpath: (attrpath
+        attr: (identifier) @function .))])
+
+(unary_expression
+  operator: _ @operator)
+
+(binary_expression
+  operator: _ @operator)
+
+(variable_expression (identifier) @variable)
+
+(binding
+  attrpath: (attrpath (identifier)) @property)
+
+"=" @operator
+
+[
+  ";"
+  "."
+  ","
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+] @punctuation.bracket
+
+(identifier) @variable
+
+((identifier) @function.builtin

crates/zed/src/languages/php/config.toml 🔗

@@ -9,3 +9,4 @@ brackets = [
     { start = "(", end = ")", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+collapsed_placeholder = "/* ... */"

crates/zed/src/languages/php/embedding.scm 🔗

@@ -0,0 +1,36 @@
+(
+    (comment)* @context
+    .
+    [
+        (function_definition
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (trait_declaration
+            "trait" @name
+            name: (_) @name)
+
+        (method_declaration
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (interface_declaration
+            "interface" @name
+            name: (_) @name
+            )
+
+        (enum_declaration
+            "enum" @name
+            name: (_) @name
+            )
+
+        ] @item
+    )

crates/zed/src/languages/php/outline.scm 🔗

@@ -8,8 +8,6 @@
     name: (_) @name
     ) @item
 
-
-
 (method_declaration
     "function" @context
     name: (_) @name
@@ -24,3 +22,8 @@
     "enum" @context
     name: (_) @name
     ) @item
+
+(trait_declaration
+    "trait" @context
+    name: (_) @name
+    ) @item

crates/zed/src/languages/ruby/config.toml 🔗

@@ -10,3 +10,4 @@ brackets = [
   { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
   { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+collapsed_placeholder = "# ..."

crates/zed/src/languages/ruby/embedding.scm 🔗

@@ -0,0 +1,22 @@
+(
+    (comment)* @context
+    .
+    [
+        (module
+            "module" @name
+            name: (_) @name)
+        (method
+            "def" @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        (class
+            "class" @name
+            name: (_) @name)
+        (singleton_method
+            "def" @name
+            object: (_) @name
+            "." @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        ] @item
+    )

crates/zed/src/languages/rust.rs 🔗

@@ -102,7 +102,7 @@ impl LspAdapter for RustLspAdapter {
         Some("rust-analyzer/flycheck".into())
     }
 
-    async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
         lazy_static! {
             static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
         }
@@ -310,7 +310,7 @@ mod tests {
                 },
             ],
         };
-        RustLspAdapter.process_diagnostics(&mut params).await;
+        RustLspAdapter.process_diagnostics(&mut params);
 
         assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 

crates/zed/src/languages/rust/config.toml 🔗

@@ -10,3 +10,4 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+collapsed_placeholder = " /* ... */ "

crates/zed/src/languages/rust/embedding.scm 🔗

@@ -1,36 +1,28 @@
-(struct_item
-    (visibility_modifier)? @context
-    "struct" @context
-    name: (_) @name) @item
+(
+    [(line_comment) (attribute_item)]* @context
+    .
+    [
+        (struct_item
+            name: (_) @name)
 
-(enum_item
-    (visibility_modifier)? @context
-    "enum" @context
-    name: (_) @name) @item
+        (enum_item
+            name: (_) @name)
 
-(impl_item
-    "impl" @context
-    trait: (_)? @name
-    "for"? @context
-    type: (_) @name) @item
+        (impl_item
+            trait: (_)? @name
+            "for"? @name
+            type: (_) @name)
 
-(trait_item
-    (visibility_modifier)? @context
-    "trait" @context
-    name: (_) @name) @item
+        (trait_item
+            name: (_) @name)
 
-(function_item
-    (visibility_modifier)? @context
-    (function_modifiers)? @context
-    "fn" @context
-    name: (_) @name) @item
+        (function_item
+            name: (_) @name
+            body: (block
+                "{" @keep
+                "}" @keep) @collapse)
 
-(function_signature_item
-    (visibility_modifier)? @context
-    (function_modifiers)? @context
-    "fn" @context
-    name: (_) @name) @item
-
-(macro_definition
-    . "macro_rules!" @context
-    name: (_) @name) @item
+        (macro_definition
+            name: (_) @name)
+        ] @item
+    )

crates/zed/src/languages/tsx/embedding.scm 🔗

@@ -1,35 +1,85 @@
-(enum_declaration
-    "enum" @context
-    name: (_) @name) @item
-
-(function_declaration
-    "async"? @context
-    "function" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+        ] @item
+    )
 
-(interface_declaration
-    "interface" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+        ] @item
+    )
 
-(program
-    (lexical_declaration
-        ["let" "const"] @context
-        (variable_declarator
-            name: (_) @name) @item))
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+        ] @item
+    )
 
-(class_declaration
-    "class" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+        ] @item
+    )
 
-(method_definition
+(
+    (comment)* @context
+    .
     [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-        ]* @context
-    name: (_) @name) @item
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+    )

crates/zed/src/languages/typescript/embedding.scm 🔗

@@ -1,59 +1,85 @@
-; (internal_module
-;     "namespace" @context
-;     name: (_) @name) @item
-
-(enum_declaration
-    "enum" @context
-    name: (_) @name) @item
-
-; (type_alias_declaration
-;     "type" @context
-;     name: (_) @name) @item
-
-(function_declaration
-    "async"? @context
-    "function" @context
-    name: (_) @name) @item
-
-(interface_declaration
-    "interface" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
 
-; (export_statement
-;     (lexical_declaration
-;         ["let" "const"] @context
-;         (variable_declarator
-;             name: (_) @name) @item))
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
 
-(program
-    (lexical_declaration
-        ["let" "const"] @context
-        (variable_declarator
-            name: (_) @name) @item))
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
 
-(class_declaration
-    "class" @context
-    name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
 
-(method_definition
+(
+    (comment)* @context
+    .
     [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-        ]* @context
-    name: (_) @name) @item
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+    ] @item
+)
 
-; (public_field_definition
-;     [
-;         "declare"
-;         "readonly"
-;         "abstract"
-;         "static"
-;         (accessibility_modifier)
-;         ]* @context
-;     name: (_) @name) @item
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

crates/zed/src/main.rs 🔗

@@ -14,7 +14,7 @@ use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext};
+use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
 use isahc::{config::Configurable, Request};
 use language::{LanguageRegistry, Point};
 use log::LevelFilter;
@@ -43,8 +43,8 @@ use std::{
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
 use sum_tree::Bias;
-use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
 use util::{
+    channel::ReleaseChannel,
     http::{self, HttpClient},
     paths::PathLikeWithPosition,
 };
@@ -55,7 +55,7 @@ use fs::RealFs;
 #[cfg(debug_assertions)]
 use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
+use workspace::AppState;
 use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -136,7 +136,7 @@ fn main() {
         languages.set_executor(cx.background().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
-        let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned());
+        let node_runtime = NodeRuntime::instance(http.clone());
 
         languages::init(languages.clone(), node_runtime.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
@@ -157,7 +157,7 @@ fn main() {
         project_panel::init(Assets, cx);
         diagnostics::init(cx);
         search::init(cx);
-        vector_store::init(fs.clone(), http.clone(), languages.clone(), cx);
+        semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
         vim::init(cx);
         terminal_view::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
@@ -415,22 +415,41 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
     panic::set_hook(Box::new(move |info| {
         let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst);
         if prior_panic_count > 0 {
-            std::panic::resume_unwind(Box::new(()));
+            // Give the panic-ing thread time to write the panic file
+            loop {
+                std::thread::yield_now();
+            }
         }
 
-        let app_version = ZED_APP_VERSION
-            .or_else(|| platform.app_version().ok())
-            .map_or("dev".to_string(), |v| v.to_string());
-
         let thread = thread::current();
-        let thread = thread.name().unwrap_or("<unnamed>");
+        let thread_name = thread.name().unwrap_or("<unnamed>");
 
-        let payload = info.payload();
-        let payload = None
-            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
-            .or_else(|| payload.downcast_ref::<String>().map(|s| s.clone()))
+        let payload = info
+            .payload()
+            .downcast_ref::<&str>()
+            .map(|s| s.to_string())
+            .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.clone()))
             .unwrap_or_else(|| "Box<Any>".to_string());
 
+        if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+            let location = info.location().unwrap();
+            let backtrace = Backtrace::new();
+            eprintln!(
+                "Thread {:?} panicked with {:?} at {}:{}:{}\n{:?}",
+                thread_name,
+                payload,
+                location.file(),
+                location.line(),
+                location.column(),
+                backtrace,
+            );
+            std::process::exit(-1);
+        }
+
+        let app_version = ZED_APP_VERSION
+            .or_else(|| platform.app_version().ok())
+            .map_or("dev".to_string(), |v| v.to_string());
+
         let backtrace = Backtrace::new();
         let mut backtrace = backtrace
             .frames()
@@ -447,7 +466,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
         }
 
         let panic_data = Panic {
-            thread: thread.into(),
+            thread: thread_name.into(),
             payload: payload.into(),
             location_data: info.location().map(|location| LocationData {
                 file: location.file().into(),
@@ -717,7 +736,7 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
 }
 
 #[cfg(not(debug_assertions))]
-fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {}
+fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
 
 fn connect_to_cli(
     server_name: &str,
@@ -902,35 +921,6 @@ async fn handle_cli_connection(
     }
 }
 
-pub fn dock_default_item_factory(
-    workspace: &mut Workspace,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<Box<dyn ItemHandle>> {
-    let strategy = settings::get::<TerminalSettings>(cx)
-        .working_directory
-        .clone();
-    let working_directory = get_working_directory(workspace, cx, strategy);
-
-    let window_id = cx.window_id();
-    let terminal = workspace
-        .project()
-        .update(cx, |project, cx| {
-            project.create_terminal(working_directory, window_id, cx)
-        })
-        .notify_err(workspace, cx)?;
-
-    let terminal_view = cx.add_view(|cx| {
-        TerminalView::new(
-            terminal,
-            workspace.weak_handle(),
-            workspace.database_id(),
-            cx,
-        )
-    });
-
-    Some(Box::new(terminal_view))
-}
-
 pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
     &[
         ("Go to file", &file_finder::Toggle),

crates/zed/src/zed.rs 🔗

@@ -179,13 +179,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
             let app_state = workspace.app_state().clone();
             let markdown = app_state.languages.language_for_name("JSON");
-            let window_id = cx.window_id();
+            let window = cx.window();
             cx.spawn(|workspace, mut cx| async move {
                 let markdown = markdown.await.log_err();
-                let content = to_string_pretty(
-                    &cx.debug_elements(window_id)
-                        .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
-                )
+                let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
+                    anyhow!("could not debug elements for window {}", window.id())
+                })?)
                 .unwrap();
                 workspace
                     .update(&mut cx, |workspace, cx| {
@@ -308,6 +307,7 @@ pub fn initialize_workspace(
             );
             let active_buffer_language =
                 cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+            let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
             let feedback_button = cx.add_view(|_| {
                 feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
             });
@@ -315,9 +315,11 @@ pub fn initialize_workspace(
             workspace.status_bar().update(cx, |status_bar, cx| {
                 status_bar.add_left_item(diagnostic_summary, cx);
                 status_bar.add_left_item(activity_indicator, cx);
+
                 status_bar.add_right_item(feedback_button, cx);
                 status_bar.add_right_item(copilot, cx);
                 status_bar.add_right_item(active_buffer_language, cx);
+                status_bar.add_right_item(vim_mode_indicator, cx);
                 status_bar.add_right_item(cursor_position, cx);
             });
 
@@ -403,29 +405,22 @@ pub fn build_window_options(
 fn quit(_: &Quit, cx: &mut gpui::AppContext) {
     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
-        let mut workspaces = cx
-            .window_ids()
+        let mut workspace_windows = cx
+            .windows()
             .into_iter()
-            .filter_map(|window_id| {
-                Some(
-                    cx.root_view(window_id)?
-                        .clone()
-                        .downcast::<Workspace>()?
-                        .downgrade(),
-                )
-            })
+            .filter_map(|window| window.downcast::<Workspace>())
             .collect::<Vec<_>>();
 
         // If multiple windows have unsaved changes, and need a save prompt,
         // prompt in the active window before switching to a different window.
-        workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
+        workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
 
-        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
-            let answer = cx.prompt(
-                workspace.window_id(),
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) {
+            let answer = window.prompt(
                 PromptLevel::Info,
                 "Are you sure you want to quit?",
                 &["Quit", "Cancel"],
+                &mut cx,
             );
 
             if let Some(mut answer) = answer {
@@ -437,14 +432,13 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) {
         }
 
         // If the user cancels any save prompt, then keep the app open.
-        for workspace in workspaces {
-            if !workspace
-                .update(&mut cx, |workspace, cx| {
-                    workspace.prepare_to_close(true, cx)
-                })?
-                .await?
-            {
-                return Ok(());
+        for window in workspace_windows {
+            if let Some(close) = window.update_root(&mut cx, |workspace, cx| {
+                workspace.prepare_to_close(false, cx)
+            }) {
+                if close.await? {
+                    return Ok(());
+                }
             }
         }
         cx.platform().quit();
@@ -542,7 +536,6 @@ pub fn handle_keymap_file_changes(
                             reload_keymaps(cx, &keymap_content);
                         }
                     })
-                    .detach();
                 }));
             }
         }
@@ -722,8 +715,8 @@ mod tests {
     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
     use fs::{FakeFs, Fs};
     use gpui::{
-        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
-        AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
+        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
+        AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
     };
     use language::LanguageRegistry;
     use node_runtime::NodeRuntime;
@@ -780,17 +773,13 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
-        let workspace_1 = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 1);
+        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(workspace.worktrees(cx).count(), 2);
             assert!(workspace.left_dock().read(cx).is_open());
@@ -807,27 +796,22 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 2);
+        assert_eq!(cx.windows().len(), 2);
 
         // Replace existing windows
-        let window_id = cx.window_ids()[0];
+        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
         cx.update(|cx| {
             open_paths(
                 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
                 &app_state,
-                Some(window_id),
+                Some(window),
                 cx,
             )
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 2);
-        let workspace_1 = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .clone()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 2);
+        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(
                 workspace
@@ -853,14 +837,11 @@ mod tests {
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         // When opening the workspace, the window is not in a edited state.
-        let workspace = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
@@ -869,19 +850,19 @@ mod tests {
                 .downcast::<Editor>()
                 .unwrap()
         });
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Editing a buffer marks the window as edited.
         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Undoing the edit restores the window's edited state.
         editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Redoing the edit marks the window as edited again.
         editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Closing the item restores the window's edited state.
         let close = pane.update(cx, |pane, cx| {
@@ -889,9 +870,10 @@ mod tests {
             pane.close_active_item(&Default::default(), cx).unwrap()
         });
         executor.run_until_parked();
-        cx.simulate_prompt_answer(workspace.window_id(), 1);
+
+        window.simulate_prompt_answer(1, cx);
         close.await.unwrap();
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Opening the buffer again doesn't impact the window's edited state.
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
@@ -904,22 +886,22 @@ mod tests {
                 .downcast::<Editor>()
                 .unwrap()
         });
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Editing the buffer marks the window as edited.
         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Ensure closing the window via the mouse gets preempted due to the
         // buffer having unsaved changes.
-        assert!(!cx.simulate_window_close(workspace.window_id()));
+        assert!(!window.simulate_close(cx));
         executor.run_until_parked();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         // The window is successfully closed after the user dismisses the prompt.
-        cx.simulate_prompt_answer(workspace.window_id(), 1);
+        window.simulate_prompt_answer(1, cx);
         executor.run_until_parked();
-        assert_eq!(cx.window_ids().len(), 0);
+        assert_eq!(cx.windows().len(), 0);
     }
 
     #[gpui::test]
@@ -932,12 +914,13 @@ mod tests {
         })
         .await;
 
-        let window_id = *cx.window_ids().first().unwrap();
-        let workspace = cx
-            .read_window(window_id, |cx| cx.root_view().clone())
+        let window = cx
+            .windows()
+            .first()
             .unwrap()
             .downcast::<Workspace>()
             .unwrap();
+        let workspace = window.root(cx);
 
         let editor = workspace.update(cx, |workspace, cx| {
             workspace
@@ -980,7 +963,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1101,12 +1085,8 @@ mod tests {
         cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
-        let workspace = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 1);
+        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
 
         #[track_caller]
         fn assert_project_panel_selection(
@@ -1292,7 +1272,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Open a file within an existing worktree.
         workspace
@@ -1318,7 +1299,7 @@ mod tests {
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
         let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
@@ -1333,11 +1314,12 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -1392,7 +1374,7 @@ mod tests {
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_and_clone(
@@ -1426,10 +1408,11 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -1477,7 +1460,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1499,7 +1483,7 @@ mod tests {
             (editor.downgrade(), buffer)
         });
 
-        cx.dispatch_action(window_id, pane::SplitRight);
+        cx.dispatch_action(window.into(), pane::SplitRight);
         let editor_2 = cx.update(|cx| {
             let pane_2 = workspace.read(cx).active_pane().clone();
             assert_ne!(pane_1, pane_2);
@@ -1509,7 +1493,7 @@ mod tests {
 
             pane2_item.downcast::<Editor>().unwrap().downgrade()
         });
-        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
 
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
@@ -1517,9 +1501,9 @@ mod tests {
             assert_eq!(workspace.active_pane(), &pane_1);
         });
 
-        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
         cx.foreground().run_until_parked();
-        cx.simulate_prompt_answer(window_id, 1);
+        window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
 
         workspace.read_with(cx, |workspace, cx| {
@@ -1551,7 +1535,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -1828,7 +1814,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -2070,11 +2058,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
 
         // Test loading the keymap base at all
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2101,7 +2089,7 @@ mod tests {
         cx.foreground().run_until_parked();
 
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &B), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2124,7 +2112,7 @@ mod tests {
         cx.foreground().run_until_parked();
 
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &B), ("[", &ActivatePrevItem)],
             line!(),
@@ -2132,7 +2120,7 @@ mod tests {
 
         #[track_caller]
         fn assert_key_bindings_for<'a>(
-            window_id: usize,
+            window: AnyWindowHandle,
             cx: &TestAppContext,
             actions: Vec<(&'static str, &'a dyn Action)>,
             line: u32,
@@ -2140,7 +2128,7 @@ mod tests {
             for (key, action) in actions {
                 // assert that...
                 assert!(
-                    cx.available_actions(window_id, 0)
+                    cx.available_actions(window, 0)
                         .into_iter()
                         .any(|(_, bound_action, b)| {
                             // action names match...
@@ -2240,11 +2228,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
 
         // Test loading the keymap base at all
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2270,7 +2258,12 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
+        assert_key_bindings_for(
+            window.into(),
+            cx,
+            vec![("k", &ActivatePreviousPane)],
+            line!(),
+        );
 
         // Test modifying the base, while retaining the users keymap
         fs.save(
@@ -2288,11 +2281,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
+        assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
 
         #[track_caller]
         fn assert_key_bindings_for<'a>(
-            window_id: usize,
+            window: AnyWindowHandle,
             cx: &TestAppContext,
             actions: Vec<(&'static str, &'a dyn Action)>,
             line: u32,
@@ -2300,7 +2293,7 @@ mod tests {
             for (key, action) in actions {
                 // assert that...
                 assert!(
-                    cx.available_actions(window_id, 0)
+                    cx.available_actions(window, 0)
                         .into_iter()
                         .any(|(_, bound_action, b)| {
                             // action names match...
@@ -2361,7 +2354,7 @@ mod tests {
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
         let http = FakeHttpClient::with_404_response();
-        let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
+        let node_runtime = NodeRuntime::instance(http);
         languages::init(languages.clone(), node_runtime);
         for name in languages.language_names() {
             languages.language_for_name(&name);

docs/theme/generating-theme-types.md 🔗

@@ -0,0 +1,29 @@
+[⬅ Back to Index](../index.md)
+
+# Generating Theme Types
+
+
+## How to generate theme types:
+
+Run a script
+
+```bash
+./script/build-theme-types
+```
+
+Types are generated in `styles/src/types/zed.ts`
+
+
+## How it works:
+
+1. Rust types
+
+    The `crates/theme` contains theme types.
+    Crate `schemars` used to generate a JSON schema from the theme structs.
+    Every struct that represent theme type has a `#[derive(JsonSchema)]` attribute.
+
+    Task lotaked at `crates/xtask/src/main.rs` generates a JSON schema from the theme structs.
+
+2. TypeScript types
+
+    Script `npm run build-types` from `styles` package generates TypeScript types from the JSON schema and saves them to `styles/src/types/zed.ts`.

rust-toolchain.toml 🔗

@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.70"
+channel = "1.71"
 components = [ "rustfmt" ]
 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

styles/src/style_tree/editor.ts 🔗

@@ -170,8 +170,8 @@ export default function editor(): any {
         line_number: with_opacity(foreground(layer), 0.35),
         line_number_active: foreground(layer),
         rename_fade: 0.6,
-        wrap_guide: with_opacity(foreground(layer), 0.1),
-        active_wrap_guide: with_opacity(foreground(layer), 0.2),
+        wrap_guide: with_opacity(foreground(layer), 0.05),
+        active_wrap_guide: with_opacity(foreground(layer), 0.1),
         unnecessary_code_fade: 0.5,
         selection: theme.players[0],
         whitespace: theme.ramps.neutral(0.5).hex(),

styles/src/style_tree/status_bar.ts 🔗

@@ -1,6 +1,8 @@
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
 import { useTheme } from "../common"
+import { text_button } from "../component/text_button"
+
 export default function status_bar(): any {
     const theme = useTheme()
 
@@ -26,32 +28,28 @@ export default function status_bar(): any {
             right: 6,
         },
         border: border(layer, { top: true, overlay: true }),
-        cursor_position: text(layer, "sans", "variant"),
-        active_language: interactive({
-            base: {
-                padding: { left: 6, right: 6 },
-                ...text(layer, "sans", "variant"),
-            },
-            state: {
-                hovered: {
-                    ...text(layer, "sans", "on"),
-                },
-            },
+        cursor_position: text(layer, "sans", "variant", { size: "xs" }),
+        vim_mode_indicator: {
+            margin: { left: 6 },
+            ...text(layer, "mono", "variant", { size: "xs" }),
+        },
+        active_language: text_button({
+            color: "variant"
         }),
-        auto_update_progress_message: text(layer, "sans", "variant"),
-        auto_update_done_message: text(layer, "sans", "variant"),
+        auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }),
+        auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }),
         lsp_status: interactive({
             base: {
                 ...diagnostic_status_container,
                 icon_spacing: 4,
                 icon_width: 14,
                 height: 18,
-                message: text(layer, "sans"),
+                message: text(layer, "sans", { size: "xs" }),
                 icon_color: foreground(layer),
             },
             state: {
                 hovered: {
-                    message: text(layer, "sans"),
+                    message: text(layer, "sans", { size: "xs" }),
                     icon_color: foreground(layer),
                     background: background(layer, "hovered"),
                 },
@@ -59,9 +57,9 @@ export default function status_bar(): any {
         }),
         diagnostic_message: interactive({
             base: {
-                ...text(layer, "sans"),
+                ...text(layer, "sans", { size: "xs" }),
             },
-            state: { hovered: text(layer, "sans", "hovered") },
+            state: { hovered: text(layer, "sans", "hovered", { size: "xs" }) },
         }),
         diagnostic_summary: interactive({
             base: {
@@ -117,7 +115,7 @@ export default function status_bar(): any {
                         icon_color: foreground(layer, "variant"),
                         label: {
                             margin: { left: 6 },
-                            ...text(layer, "sans", { size: "sm" }),
+                            ...text(layer, "sans", { size: "xs" }),
                         },
                     },
                     state: {

styles/src/theme/create_theme.ts 🔗

@@ -1,4 +1,4 @@
-import { Scale, Color } from "chroma-js"
+import chroma, { Scale, Color } from "chroma-js"
 import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
 export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
 import {
@@ -32,6 +32,7 @@ export interface Theme {
 
     players: Players
     syntax?: Partial<ThemeSyntax>
+    color_family: ColorFamily
 }
 
 export interface Meta {
@@ -69,6 +70,15 @@ export interface Players {
     "7": Player
 }
 
+export type ColorFamily = Partial<{ [K in keyof RampSet]: ColorFamilyRange }>
+
+export interface ColorFamilyRange {
+    low: number
+    high: number
+    range: number
+    scaling_value: number
+}
+
 export interface Shadow {
     blur: number
     color: string
@@ -162,6 +172,8 @@ export function create_theme(theme: ThemeConfig): Theme {
         "7": player(ramps.yellow),
     }
 
+    const color_family = build_color_family(ramps)
+
     return {
         name,
         is_light,
@@ -177,6 +189,7 @@ export function create_theme(theme: ThemeConfig): Theme {
 
         players,
         syntax,
+        color_family,
     }
 }
 
@@ -187,6 +200,28 @@ function player(ramp: Scale): Player {
     }
 }
 
+function build_color_family(ramps: RampSet): ColorFamily {
+    const color_family: ColorFamily = {}
+
+    for (const ramp in ramps) {
+        const ramp_value = ramps[ramp as keyof RampSet]
+
+        const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100]
+        const low = Math.min(...lightnessValues)
+        const high = Math.max(...lightnessValues)
+        const range = high - low
+
+        color_family[ramp as keyof RampSet] = {
+            low,
+            high,
+            range,
+            scaling_value: 100 / range,
+        }
+    }
+
+    return color_family
+}
+
 function lowest_layer(ramps: RampSet): Layer {
     return {
         base: build_style_set(ramps.neutral, 0.2, 1),