Merge branch 'paint-context' into cells

Nathan Sobo created

Change summary

Cargo.lock                                                                     | 367 
Cargo.toml                                                                     |  25 
assets/icons/file_icons/archive.svg                                            |   5 
assets/icons/file_icons/audio.svg                                              |   6 
assets/icons/file_icons/book.svg                                               |   6 
assets/icons/file_icons/camera.svg                                             |   4 
assets/icons/file_icons/chevron_down.svg                                       |   3 
assets/icons/file_icons/chevron_left.svg                                       |   3 
assets/icons/file_icons/chevron_right.svg                                      |   3 
assets/icons/file_icons/chevron_up.svg                                         |   3 
assets/icons/file_icons/code.svg                                               |   4 
assets/icons/file_icons/database.svg                                           |   5 
assets/icons/file_icons/eslint.svg                                             |   4 
assets/icons/file_icons/file.svg                                               |   5 
assets/icons/file_icons/file_types.json                                        | 159 
assets/icons/file_icons/folder.svg                                             |   5 
assets/icons/file_icons/folder_open.svg                                        |   5 
assets/icons/file_icons/git.svg                                                |   6 
assets/icons/file_icons/hash.svg                                               |   6 
assets/icons/file_icons/html.svg                                               |   5 
assets/icons/file_icons/image.svg                                              |   7 
assets/icons/file_icons/info.svg                                               |   3 
assets/icons/file_icons/lock.svg                                               |   6 
assets/icons/file_icons/notebook.svg                                           |   8 
assets/icons/file_icons/package.svg                                            |   4 
assets/icons/file_icons/prettier.svg                                           |  12 
assets/icons/file_icons/rust.svg                                               |   2 
assets/icons/file_icons/settings.svg                                           |   1 
assets/icons/file_icons/terminal.svg                                           |   5 
assets/icons/file_icons/toml.svg                                               |   5 
assets/icons/file_icons/typescript.svg                                         |   2 
assets/icons/file_icons/video.svg                                              |   4 
assets/keymaps/atom.json                                                       |   1 
assets/keymaps/default.json                                                    |  35 
assets/keymaps/jetbrains.json                                                  |   3 
assets/keymaps/sublime_text.json                                               |   1 
assets/keymaps/textmate.json                                                   |   1 
assets/keymaps/vim.json                                                        | 179 
assets/settings/default.json                                                   |  43 
crates/ai/src/assistant.rs                                                     |  22 
crates/call/Cargo.toml                                                         |   4 
crates/call/src/call.rs                                                        |  19 
crates/call/src/call_settings.rs                                               |  27 
crates/call/src/room.rs                                                        |  38 
crates/client/src/telemetry.rs                                                 |   2 
crates/collab/src/tests/integration_tests.rs                                   |   6 
crates/collab_ui/src/collab_titlebar_item.rs                                   |  14 
crates/collab_ui/src/collab_ui.rs                                              |  38 
crates/collab_ui/src/contact_finder.rs                                         |   2 
crates/collab_ui/src/face_pile.rs                                              |   4 
crates/command_palette/src/command_palette.rs                                  |   2 
crates/db/src/db.rs                                                            | 100 
crates/editor/Cargo.toml                                                       |  18 
crates/editor/src/editor.rs                                                    | 190 
crates/editor/src/editor_tests.rs                                              | 154 
crates/editor/src/element.rs                                                   |  72 
crates/editor/src/hover_popover.rs                                             | 116 
crates/editor/src/inlay_hint_cache.rs                                          | 375 
crates/editor/src/items.rs                                                     | 100 
crates/editor/src/link_go_to_definition.rs                                     |  15 
crates/editor/src/selections_collection.rs                                     |   6 
crates/feedback/src/feedback_editor.rs                                         |  34 
crates/feedback/src/submit_feedback_button.rs                                  |  22 
crates/file_finder/src/file_finder.rs                                          |  90 
crates/fs/src/repository.rs                                                    | 164 
crates/gpui/src/app.rs                                                         | 115 
crates/gpui/src/app/window.rs                                                  |  28 
crates/gpui/src/elements.rs                                                    |  26 
crates/gpui/src/elements/align.rs                                              |   5 
crates/gpui/src/elements/canvas.rs                                             |   4 
crates/gpui/src/elements/clipped.rs                                            |   5 
crates/gpui/src/elements/constrained_box.rs                                    |   5 
crates/gpui/src/elements/container.rs                                          |   5 
crates/gpui/src/elements/empty.rs                                              |   4 
crates/gpui/src/elements/expanded.rs                                           |   5 
crates/gpui/src/elements/flex.rs                                               |   8 
crates/gpui/src/elements/hook.rs                                               |   5 
crates/gpui/src/elements/image.rs                                              |   6 
crates/gpui/src/elements/keystroke_label.rs                                    |   2 
crates/gpui/src/elements/label.rs                                              |   4 
crates/gpui/src/elements/list.rs                                               |  10 
crates/gpui/src/elements/mouse_event_handler.rs                                |   6 
crates/gpui/src/elements/overlay.rs                                            |   6 
crates/gpui/src/elements/resizable.rs                                          |   6 
crates/gpui/src/elements/stack.rs                                              |   5 
crates/gpui/src/elements/svg.rs                                                |   3 
crates/gpui/src/elements/text.rs                                               |   6 
crates/gpui/src/elements/tooltip.rs                                            |   6 
crates/gpui/src/elements/uniform_list.rs                                       |   4 
crates/gpui/src/fonts.rs                                                       |  26 
crates/gpui/src/keymap_matcher.rs                                              |  11 
crates/gpui/src/keymap_matcher/binding.rs                                      |   4 
crates/gpui/src/keymap_matcher/keymap.rs                                       | 389 
crates/gpui/src/keymap_matcher/keymap_context.rs                               |   2 
crates/gpui/src/keymap_matcher/keystroke.rs                                    |   2 
crates/gpui/src/platform/mac/platform.rs                                       |   2 
crates/gpui/src/platform/mac/window.rs                                         |  46 
crates/language/Cargo.toml                                                     |  26 
crates/language/src/buffer.rs                                                  |  34 
crates/language/src/buffer_tests.rs                                            | 158 
crates/language/src/language.rs                                                |   3 
crates/language/src/language_settings.rs                                       |   9 
crates/language/src/syntax_map.rs                                              | 117 
crates/language/src/syntax_map/syntax_map_tests.rs                             | 235 
crates/language_selector/src/language_selector.rs                              |   2 
crates/language_tools/src/lsp_log.rs                                           |  14 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift |  46 
crates/live_kit_client/examples/test_app.rs                                    |   2 
crates/live_kit_client/src/prod.rs                                             |  32 
crates/live_kit_client/src/test.rs                                             |  17 
crates/lsp/src/lsp.rs                                                          |   7 
crates/menu/src/menu.rs                                                        |   1 
crates/node_runtime/src/node_runtime.rs                                        |  54 
crates/outline/src/outline.rs                                                  |   2 
crates/picker/src/picker.rs                                                    |  20 
crates/project/src/project.rs                                                  |  14 
crates/project/src/worktree.rs                                                 | 316 
crates/project_panel/Cargo.toml                                                |   1 
crates/project_panel/src/file_associations.rs                                  | 103 
crates/project_panel/src/project_panel.rs                                      | 334 
crates/project_panel/src/project_panel_settings.rs                             |  14 
crates/project_symbols/src/project_symbols.rs                                  |   9 
crates/recent_projects/src/recent_projects.rs                                  |   2 
crates/search/Cargo.toml                                                       |   1 
crates/search/src/buffer_search.rs                                             | 595 
crates/search/src/project_search.rs                                            | 225 
crates/search/src/search.rs                                                    |  46 
crates/terminal/src/terminal.rs                                                | 178 
crates/terminal_view/src/terminal_element.rs                                   |  33 
crates/terminal_view/src/terminal_panel.rs                                     |  12 
crates/terminal_view/src/terminal_view.rs                                      | 174 
crates/theme/src/theme.rs                                                      |  12 
crates/theme/src/theme_registry.rs                                             |  22 
crates/theme/src/theme_settings.rs                                             |   4 
crates/theme_selector/src/theme_selector.rs                                    |   2 
crates/vcs_menu/src/lib.rs                                                     |  16 
crates/vector_store/src/embedding.rs                                           |  12 
crates/vector_store/src/modal.rs                                               |   2 
crates/vector_store/src/parsing.rs                                             |   7 
crates/vim/src/editor_events.rs                                                |   4 
crates/vim/src/motion.rs                                                       |  69 
crates/vim/src/normal.rs                                                       |  27 
crates/vim/src/normal/search.rs                                                | 302 
crates/vim/src/state.rs                                                        |  32 
crates/vim/src/test.rs                                                         |  46 
crates/vim/src/test/vim_test_context.rs                                        |   1 
crates/vim/src/vim.rs                                                          |  38 
crates/vim/src/visual.rs                                                       |   4 
crates/vim/test_data/test_comma_semicolon.json                                 |  17 
crates/welcome/src/base_keymap_picker.rs                                       |   2 
crates/workspace/src/item.rs                                                   |  55 
crates/workspace/src/pane.rs                                                   | 191 
crates/workspace/src/pane_group.rs                                             | 200 
crates/workspace/src/searchable.rs                                             |  42 
crates/workspace/src/status_bar.rs                                             |   6 
crates/workspace/src/workspace.rs                                              | 279 
crates/zed/Cargo.toml                                                          |  47 
crates/zed/src/languages.rs                                                    |   9 
crates/zed/src/languages/bash/brackets.scm                                     |   3 
crates/zed/src/languages/bash/config.toml                                      |   8 
crates/zed/src/languages/bash/highlights.scm                                   |  58 
crates/zed/src/languages/elm/config.toml                                       |  11 
crates/zed/src/languages/elm/highlights.scm                                    |  72 
crates/zed/src/languages/elm/injections.scm                                    |   2 
crates/zed/src/languages/elm/outline.scm                                       |  22 
crates/zed/src/languages/glsl/config.toml                                      |   9 
crates/zed/src/languages/glsl/highlights.scm                                   | 118 
crates/zed/src/languages/heex/config.toml                                      |   2 
crates/zed/src/languages/heex/highlights.scm                                   |   6 
crates/zed/src/languages/php.rs                                                | 133 
crates/zed/src/languages/php/config.toml                                       |  11 
crates/zed/src/languages/php/highlights.scm                                    | 123 
crates/zed/src/languages/php/injections.scm                                    |   3 
crates/zed/src/languages/php/outline.scm                                       |  26 
crates/zed/src/languages/php/tags.scm                                          |  40 
crates/zed/src/languages/python/outline.scm                                    |   2 
crates/zed/src/main.rs                                                         |  47 
crates/zed/src/menus.rs                                                        |   1 
crates/zed/src/zed.rs                                                          | 101 
script/generate-licenses                                                       |   2 
script/zed-with-local-servers                                                  |   2 
styles/src/style_tree/editor.ts                                                |   4 
styles/src/style_tree/feedback.ts                                              |   5 
styles/src/style_tree/project_panel.ts                                         |  26 
styles/src/style_tree/search.ts                                                |  29 
styles/src/style_tree/tab_bar.ts                                               |  14 
186 files changed, 6,754 insertions(+), 1,756 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -36,11 +36,11 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.20.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
 dependencies = [
- "gimli 0.27.3",
+ "gimli 0.27.2",
 ]
 
 [[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.10",
+ "getrandom 0.2.9",
  "once_cell",
  "version_check",
 ]
@@ -88,9 +88,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "1.0.2"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
 dependencies = [
  "memchr",
 ]
@@ -118,7 +118,7 @@ dependencies = [
  "settings",
  "smol",
  "theme",
- "tiktoken-rs 0.4.5",
+ "tiktoken-rs 0.4.2",
  "util",
  "workspace",
 ]
@@ -151,7 +151,7 @@ dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
  "base64 0.13.1",
- "bitflags 1.3.2",
+ "bitflags",
  "dirs 4.0.0",
  "libc",
  "log",
@@ -177,12 +177,6 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
-[[package]]
-name = "allocator-api2"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9"
-
 [[package]]
 name = "alsa"
 version = "0.7.0"
@@ -190,7 +184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
 dependencies = [
  "alsa-sys",
- "bitflags 1.3.2",
+ "bitflags",
  "libc",
  "nix",
 ]
@@ -211,12 +205,6 @@ version = "0.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
 
-[[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"
@@ -237,7 +225,7 @@ dependencies = [
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
- "is-terminal 0.4.9",
+ "is-terminal 0.4.7",
  "utf8parse",
 ]
 
@@ -262,7 +250,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -272,7 +260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
 dependencies = [
  "anstyle",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -295,9 +283,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.4"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 
 [[package]]
 name = "ascii"
@@ -318,9 +306,9 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "1.9.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
 dependencies = [
  "concurrent-queue",
  "event-listener",
@@ -336,7 +324,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "once_cell",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "tokio",
 ]
 
@@ -350,7 +338,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "memchr",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
 ]
 
 [[package]]
@@ -374,7 +362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
 dependencies = [
  "async-lock",
- "autocfg",
+ "autocfg 1.1.0",
  "blocking",
  "futures-lite",
 ]
@@ -401,14 +389,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
 dependencies = [
  "async-lock",
- "autocfg",
+ "autocfg 1.1.0",
  "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-lite",
  "log",
  "parking",
  "polling",
- "rustix 0.37.23",
+ "rustix 0.37.19",
  "slab",
  "socket2",
  "waker-fn",
@@ -430,7 +418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
 dependencies = [
  "async-io",
- "autocfg",
+ "autocfg 1.1.0",
  "blocking",
  "futures-lite",
 ]
@@ -452,14 +440,14 @@ checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
 dependencies = [
  "async-io",
  "async-lock",
- "autocfg",
+ "autocfg 1.1.0",
  "blocking",
  "cfg-if 1.0.0",
  "event-listener",
  "futures-lite",
- "rustix 0.37.23",
+ "rustix 0.37.19",
  "signal-hook",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -481,7 +469,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -494,7 +482,7 @@ dependencies = [
  "async-global-executor",
  "async-io",
  "async-lock",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.15",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -504,7 +492,7 @@ dependencies = [
  "log",
  "memchr",
  "once_cell",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "pin-utils",
  "slab",
  "wasm-bindgen-futures",
@@ -518,7 +506,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
 ]
 
 [[package]]
@@ -529,7 +517,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -566,13 +554,13 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.71"
+version = "0.1.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -585,7 +573,7 @@ dependencies = [
  "futures-io",
  "futures-util",
  "log",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "tungstenite 0.16.0",
 ]
 
@@ -600,9 +588,12 @@ dependencies = [
 
 [[package]]
 name = "atomic"
-version = "0.5.3"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
+dependencies = [
+ "autocfg 1.1.0",
+]
 
 [[package]]
 name = "atomic-waker"
@@ -658,6 +649,15 @@ 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 +673,19 @@ dependencies = [
  "async-trait",
  "axum-core",
  "base64 0.13.1",
- "bitflags 1.3.2",
+ "bitflags",
  "bytes 1.4.0",
  "futures-util",
  "headers",
  "http",
  "http-body",
  "hyper",
- "itoa 1.0.8",
+ "itoa 1.0.6",
  "matchit",
  "memchr",
  "mime",
  "percent-encoding",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -726,7 +726,7 @@ dependencies = [
  "futures-util",
  "http",
  "mime",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "serde",
  "serde_json",
  "tokio",
@@ -738,16 +738,16 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.68"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
 dependencies = [
- "addr2line 0.20.0",
+ "addr2line 0.19.0",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.7.1",
- "object 0.31.1",
+ "miniz_oxide 0.6.2",
+ "object 0.30.3",
  "rustc-demangle",
 ]
 
@@ -797,7 +797,7 @@ version = "0.64.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -817,7 +817,7 @@ version = "0.65.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -830,7 +830,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.25",
+ "syn 2.0.18",
  "which",
 ]
 
@@ -855,24 +855,6 @@ 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"
-
-[[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"
@@ -998,15 +980,15 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.13.0"
+version = "3.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
 
 [[package]]
 name = "bytecheck"
-version = "0.6.11"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
+checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f"
 dependencies = [
  "bytecheck_derive",
  "ptr_meta",
@@ -1015,9 +997,9 @@ dependencies = [
 
 [[package]]
 name = "bytecheck_derive"
-version = "0.6.11"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61"
+checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1070,6 +1052,10 @@ dependencies = [
  "media",
  "postage",
  "project",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
  "settings",
  "util",
 ]
@@ -1184,13 +1170,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.26"
+version = "0.4.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
 dependencies = [
- "android-tzdata",
  "iana-time-zone",
  "js-sys",
+ "num-integer",
  "num-traits",
  "serde",
  "time 0.1.45",
@@ -1221,7 +1207,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
 dependencies = [
  "glob",
  "libc",
- "libloading 0.7.4",
+ "libloading",
 ]
 
 [[package]]
@@ -1231,7 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
  "atty",
- "bitflags 1.3.2",
+ "bitflags",
  "clap_derive 3.2.25",
  "clap_lex 0.2.4",
  "indexmap 1.9.3",
@@ -1243,9 +1229,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.3.11"
+version = "4.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
+checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
 dependencies = [
  "clap_builder",
  "clap_derive 4.3.2",
@@ -1254,12 +1240,13 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.3.11"
+version = "4.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
+checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
 dependencies = [
  "anstream",
  "anstyle",
+ "bitflags",
  "clap_lex 0.5.0",
  "strsim",
 ]
@@ -1286,7 +1273,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -1353,11 +1340,11 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.23",
+ "time 0.3.21",
  "tiny_http",
  "url",
  "util",
- "uuid 1.4.0",
+ "uuid 1.3.2",
 ]
 
 [[package]]
@@ -1381,7 +1368,7 @@ name = "cocoa"
 version = "0.24.0"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "block",
  "cocoa-foundation",
  "core-foundation",
@@ -1396,7 +1383,7 @@ name = "cocoa-foundation"
 version = "0.1.1"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "block",
  "core-foundation",
  "core-graphics-types",
@@ -1405,6 +1392,16 @@ 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"
@@ -1455,7 +1452,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.23",
+ "time 0.3.21",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1557,7 +1554,7 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
 dependencies = [
- "crossbeam-utils",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
@@ -1648,7 +1645,7 @@ name = "core-graphics"
 version = "0.22.3"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "core-foundation",
  "core-graphics-types",
  "foreign-types",
@@ -1660,7 +1657,7 @@ name = "core-graphics-types"
 version = "0.1.1"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "core-foundation",
  "foreign-types",
  "libc",
@@ -1693,7 +1690,7 @@ version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "core-foundation-sys 0.6.2",
  "coreaudio-sys",
 ]
@@ -1743,9 +1740,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.9"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
 dependencies = [
  "libc",
 ]
@@ -1870,6 +1867,16 @@ 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"
@@ -1877,7 +1884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
@@ -1888,19 +1895,19 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.15"
+version = "0.9.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
 dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
  "cfg-if 1.0.0",
- "crossbeam-utils",
- "memoffset 0.9.0",
+ "crossbeam-utils 0.8.15",
+ "memoffset 0.8.0",
  "scopeguard",
 ]
 
@@ -1911,14 +1918,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.16"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if 0.1.10",
+ "lazy_static",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1970,9 +1988,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.63+curl-8.1.2"
+version = "0.4.61+curl-8.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
+checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
 dependencies = [
  "cc",
  "libc",
@@ -1983,17 +2001,61 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
+]
+
 [[package]]
 name = "dashmap"
-version = "5.5.0"
+version = "5.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
 dependencies = [
  "cfg-if 1.0.0",
- "hashbrown 0.14.0",
+ "hashbrown 0.12.3",
  "lock_api",
  "once_cell",
- "parking_lot_core 0.9.8",
+ "parking_lot_core 0.9.7",
 ]
 
 [[package]]
@@ -2098,9 +2160,9 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.7"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
  "block-buffer 0.10.4",
  "crypto-common",
@@ -2169,11 +2231,11 @@ dependencies = [
 
 [[package]]
 name = "dlib"
-version = "0.5.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794"
 dependencies = [
- "libloading 0.8.0",
+ "libloading",
 ]
 
 [[package]]
@@ -2253,9 +2315,8 @@ dependencies = [
  "theme",
  "tree-sitter",
  "tree-sitter-html",
- "tree-sitter-javascript",
  "tree-sitter-rust",
- "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)",
+ "tree-sitter-typescript",
  "unindent",
  "util",
  "workspace",
@@ -2296,7 +2357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
 dependencies = [
  "humantime",
- "is-terminal 0.4.9",
+ "is-terminal 0.4.7",
  "log",
  "regex",
  "termcolor",
@@ -2313,15 +2374,15 @@ dependencies = [
 
 [[package]]
 name = "equivalent"
-version = "1.0.1"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.27"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
+checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569"
 dependencies = [
  "serde",
 ]
@@ -2345,7 +2406,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2360,9 +2421,9 @@ dependencies = [
 
 [[package]]
 name = "etagere"
-version = "0.2.8"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
+checksum = "6301151a318f367f392c31395beb1cfba5ccd9abc44d1db0db3a4b27b9601c89"
 dependencies = [
  "euclid",
  "svg_fmt",
@@ -2485,7 +2546,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.2.16",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2539,7 +2600,7 @@ name = "font-kit"
 version = "0.11.0"
 source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "byteorder",
  "core-foundation",
  "core-graphics",
@@ -2586,9 +2647,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "form_urlencoded"
-version = "1.2.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
 dependencies = [
  "percent-encoding",
 ]
@@ -2639,7 +2700,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
- "time 0.3.23",
+ "time 0.3.21",
  "util",
 ]
 
@@ -2658,7 +2719,7 @@ dependencies = [
 name = "fsevent"
 version = "2.0.2"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "fsevent-sys",
  "parking_lot 0.11.2",
  "tempdir",
@@ -2685,7 +2746,7 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "fuchsia-zircon-sys",
 ]
 
@@ -2695,12 +2756,6 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
-[[package]]
-name = "funty"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
-
 [[package]]
 name = "futures"
 version = "0.1.31"
@@ -2777,7 +2832,7 @@ dependencies = [
  "futures-io",
  "memchr",
  "parking",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "waker-fn",
 ]
 
@@ -2789,7 +2844,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -2818,7 +2873,7 @@ dependencies = [
  "futures-sink",
  "futures-task",
  "memchr",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "pin-utils",
  "slab",
  "tokio-io",
@@ -2864,9 +2919,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.10"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
@@ -2896,9 +2951,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.27.3"
+version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
 
 [[package]]
 name = "git"

Cargo.toml 🔗

@@ -109,6 +109,31 @@ tree-sitter = "0.20"
 unindent = { version = "0.1.7" }
 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-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-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" }
+tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
+tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
+tree-sitter-rust = "0.20.3"
+tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
+tree-sitter-php = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "d43130fd1525301e9826f420c5393a4d169819fc" }
+tree-sitter-python = "0.20.2"
+tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
+tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
+tree-sitter-ruby = "0.20.0"
+tree-sitter-html = "0.19.0"
+tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
+tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca"}
+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"
+
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }

assets/icons/file_icons/archive.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="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="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 🔗

@@ -0,0 +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"/>
+<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/book.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 10L12 10.8374C12 10.9431 11.9665 11.046 11.9044 11.1315L11.1498 12.1691C11.0557 12.2985 10.9054 12.375 10.7454 12.375L3.25461 12.375C3.09464 12.375 2.94433 12.2985 2.85024 12.1691L2.09563 11.1315C2.03348 11.046 2 10.9431 2 10.8374L2 2" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 12V10L7 11H12V12H2Z" fill="black"/>
+<path d="M5.63246 2.04415C6.44914 2.31638 7 3.08066 7 3.94152V10.7306C7 11.0924 6.62757 11.3345 6.29693 11.1875L2.79693 9.63197C2.61637 9.55172 2.5 9.37266 2.5 9.17506V1.69371C2.5 1.35243 2.83435 1.11145 3.15811 1.21937L5.63246 2.04415Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.5 2C7.67157 2 7 2.67157 7 3.5V12C7 11.1954 10.2366 11.0382 11.5017 11.0075C11.7778 11.0008 12 10.7761 12 10.5V2.5C12 2.22386 11.7761 2 11.5 2H8.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/camera.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 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"/>
+</svg>

assets/icons/file_icons/chevron_down.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="M3.63281 5.66406L6.99344 8.89844L10.3672 5.66406" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/chevron_left.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="M8.35938 3.63281L5.125 6.99344L8.35938 10.3672" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/chevron_right.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="M5.64062 3.64062L8.89062 7.00125L5.64062 10.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/chevron_up.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="M3.63281 8.36719L6.99344 5.13281L10.3672 8.36719" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/code.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="M4.375 2C2.5 2 2.5 3.5 2.5 4.5C2.5 5.5 2 6.50106 1 7C2 7.50106 2.5 8.5 2.5 9.5C2.5 10.5 2.5 12 4.375 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.63281 2C11.5078 2 11.5078 3.5 11.5078 4.5C11.5078 5.5 12.0078 6.50106 13.0078 7C12.0078 7.50106 11.5078 8.5 11.5078 9.5C11.5078 10.5 11.5078 12 9.63281 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/database.svg 🔗

@@ -0,0 +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"/>
+<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/eslint.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="M12.5413 7.3125C12.6529 7.11913 12.6529 6.88088 12.5413 6.6875L10.0413 2.35738C9.92962 2.164 9.72329 2.04488 9.5 2.04488L4.5 2.04488C4.27671 2.04488 4.07038 2.164 3.95873 2.35738L1.45873 6.6875C1.34709 6.88088 1.34709 7.11913 1.45873 7.3125L3.95873 11.6426C4.07038 11.836 4.27671 11.9551 4.5 11.9551L9.5 11.9551C9.72329 11.9551 9.92962 11.836 10.0413 11.6426L12.5413 7.3125Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M6.75 4.14434C6.9047 4.05502 7.0953 4.05502 7.25 4.14434L9.34808 5.35566C9.50278 5.44498 9.59808 5.61004 9.59808 5.78868V8.21132C9.59808 8.38996 9.50278 8.55502 9.34808 8.64434L7.25 9.85566C7.0953 9.94498 6.9047 9.94498 6.75 9.85566L4.65192 8.64434C4.49722 8.55502 4.40192 8.38996 4.40192 8.21132L4.40192 5.78868C4.40192 5.61004 4.49722 5.44498 4.65192 5.35566L6.75 4.14434Z" fill="black"/>
+</svg>

assets/icons/file_icons/file.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 4H10" stroke="black" stroke-opacity="0.66" 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"/>
+</svg>

assets/icons/file_icons/file_types.json 🔗

@@ -0,0 +1,159 @@
+{
+  "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"
+    }
+  }
+}

assets/icons/file_icons/folder.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 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"/>
+</svg>

assets/icons/file_icons/folder_open.svg 🔗

@@ -0,0 +1,5 @@
+<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"/>
+</svg>

assets/icons/file_icons/git.svg 🔗

@@ -0,0 +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"/>
+<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/hash.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<line x1="10.2795" y1="2.63847" x2="7.74785" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="6.26624" y1="2.99597" x2="3.7346" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="3.15982" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="2.0983" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/html.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="M8.15732 3.17108L5.84268 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/image.svg 🔗

@@ -0,0 +1,7 @@
+<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="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"/>
+</svg>

assets/icons/file_icons/info.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="M7 7L7 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 5.25C7.34518 5.25 7.625 4.97018 7.625 4.625C7.625 4.27982 7.34518 4 7 4C6.65482 4 6.375 4.27982 6.375 4.625C6.375 4.97018 6.65482 5.25 7 5.25Z" fill="black"/>

assets/icons/file_icons/lock.svg 🔗

@@ -0,0 +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"/>
+<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/notebook.svg 🔗

@@ -0,0 +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"/>
+<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="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/package.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="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"/>
+</svg>

assets/icons/file_icons/prettier.svg 🔗

@@ -0,0 +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="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="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 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"/>
+</svg>

assets/icons/file_icons/rust.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="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/terminal.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="M1.65625 2.5C1.65625 2.22386 1.88011 2 2.15625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V11.5C12.3438 11.7761 12.1199 12 11.8437 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V2.5Z" stroke="black" stroke-width="1.25"/>
+<path d="M4.375 9L6.375 7L4.375 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.625 9L9.90625 9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/toml.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="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"/>
+</svg>

assets/icons/file_icons/typescript.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="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"/>

assets/icons/file_icons/video.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="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"/>
+</svg>

assets/keymaps/atom.json 🔗

@@ -9,6 +9,7 @@
     "context": "Editor",
     "bindings": {
       "cmd-b": "editor::GoToDefinition",
+      "alt-cmd-b": "editor::GoToDefinitionSplit",
       "cmd-<": "editor::ScrollCursorCenter",
       "cmd-g": [
         "editor::SelectNext",

assets/keymaps/default.json 🔗

@@ -13,6 +13,7 @@
       "cmd-up": "menu::SelectFirst",
       "cmd-down": "menu::SelectLast",
       "enter": "menu::Confirm",
+      "cmd-enter": "menu::SecondaryConfirm",
       "escape": "menu::Cancel",
       "ctrl-c": "menu::Cancel",
       "cmd-{": "pane::ActivatePrevItem",
@@ -194,8 +195,8 @@
   {
     "context": "Editor && mode == auto_height",
     "bindings": {
-      "alt-enter": "editor::Newline",
-      "cmd-alt-enter": "editor::NewlineBelow"
+      "ctrl-enter": "editor::Newline",
+      "ctrl-shift-enter": "editor::NewlineBelow"
     }
   },
   {
@@ -221,7 +222,8 @@
       "escape": "buffer_search::Dismiss",
       "tab": "buffer_search::FocusEditor",
       "enter": "search::SelectNextMatch",
-      "shift-enter": "search::SelectPrevMatch"
+      "shift-enter": "search::SelectPrevMatch",
+      "alt-enter": "search::SelectAllMatches"
     }
   },
   {
@@ -242,6 +244,7 @@
       "cmd-f": "project_search::ToggleFocus",
       "cmd-g": "search::SelectNextMatch",
       "cmd-shift-g": "search::SelectPrevMatch",
+      "alt-enter": "search::SelectAllMatches",
       "alt-cmd-c": "search::ToggleCaseSensitive",
       "alt-cmd-w": "search::ToggleWholeWord",
       "alt-cmd-r": "search::ToggleRegex"
@@ -296,7 +299,9 @@
       "shift-f8": "editor::GoToPrevDiagnostic",
       "f2": "editor::Rename",
       "f12": "editor::GoToDefinition",
+      "alt-f12": "editor::GoToDefinitionSplit",
       "cmd-f12": "editor::GoToTypeDefinition",
+      "alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
       "alt-shift-f12": "editor::FindAllReferences",
       "ctrl-m": "editor::MoveToEnclosingBracket",
       "alt-cmd-[": "editor::Fold",
@@ -401,6 +406,7 @@
       "cmd-b": "workspace::ToggleLeftDock",
       "cmd-r": "workspace::ToggleRightDock",
       "cmd-j": "workspace::ToggleBottomDock",
+      "alt-cmd-y": "workspace::CloseAllDocks",
       "cmd-shift-f": "workspace::NewSearch",
       "cmd-k cmd-t": "theme_selector::Toggle",
       "cmd-k cmd-s": "zed::OpenKeymap",
@@ -441,8 +447,22 @@
   },
   {
     "bindings": {
-      "cmd-k cmd-left": "workspace::ActivatePreviousPane",
-      "cmd-k cmd-right": "workspace::ActivateNextPane"
+      "cmd-k cmd-left": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "cmd-k cmd-right": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "cmd-k cmd-up": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "cmd-k cmd-down": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ]
     }
   },
   // Bindings from Atom
@@ -508,8 +528,11 @@
       "cmd-alt-c": "project_panel::CopyPath",
       "alt-cmd-shift-c": "project_panel::CopyRelativePath",
       "f2": "project_panel::Rename",
+      "enter": "project_panel::Rename",
+      "space": "project_panel::Open",
       "backspace": "project_panel::Delete",
-      "alt-cmd-r": "project_panel::RevealInFinder"
+      "alt-cmd-r": "project_panel::RevealInFinder",
+      "alt-shift-f": "project_panel::NewSearchInDirectory"
     }
   },
   {

assets/keymaps/jetbrains.json 🔗

@@ -46,8 +46,9 @@
       "alt-f7": "editor::FindAllReferences",
       "cmd-alt-f7": "editor::FindAllReferences",
       "cmd-b": "editor::GoToDefinition",
-      "cmd-alt-b": "editor::GoToDefinition",
+      "cmd-alt-b": "editor::GoToDefinitionSplit",
       "cmd-shift-b": "editor::GoToTypeDefinition",
+      "cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
       "alt-enter": "editor::ToggleCodeActions",
       "f2": "editor::GoToDiagnostic",
       "cmd-f2": "editor::GoToPrevDiagnostic",

assets/keymaps/sublime_text.json 🔗

@@ -20,6 +20,7 @@
       "cmd-shift-a": "editor::SelectLargerSyntaxNode",
       "shift-f12": "editor::FindAllReferences",
       "alt-cmd-down": "editor::GoToDefinition",
+      "ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
       "alt-shift-cmd-down": "editor::FindAllReferences",
       "ctrl-.": "editor::GoToHunk",
       "ctrl-,": "editor::GoToPrevHunk",

assets/keymaps/textmate.json 🔗

@@ -12,6 +12,7 @@
       "cmd-l": "go_to_line::Toggle",
       "ctrl-shift-d": "editor::DuplicateLine",
       "cmd-b": "editor::GoToDefinition",
+      "alt-cmd-b": "editor::GoToDefinition",
       "cmd-j": "editor::ScrollCursorCenter",
       "cmd-shift-l": "editor::SelectLine",
       "cmd-shift-t": "outline::Toggle",

assets/keymaps/vim.json 🔗

@@ -2,12 +2,6 @@
   {
     "context": "Editor && VimControl && !VimWaiting && !menu",
     "bindings": {
-      "g": [
-        "vim::PushOperator",
-        {
-          "Namespace": "G"
-        }
-      ],
       "i": [
         "vim::PushOperator",
         {
@@ -30,6 +24,8 @@
       "j": "vim::Down",
       "down": "vim::Down",
       "enter": "vim::NextLineStart",
+      "tab": "vim::Tab",
+      "shift-tab": "vim::Tab",
       "k": "vim::Up",
       "up": "vim::Up",
       "l": "vim::Right",
@@ -60,6 +56,8 @@
           "ignorePunctuation": true
         }
       ],
+      "n": "search::SelectNextMatch",
+      "shift-n": "search::SelectPrevMatch",
       "%": "vim::Matching",
       "f": [
         "vim::PushOperator",
@@ -99,7 +97,39 @@
         "vim::SwitchMode",
         "Normal"
       ],
+      "ctrl+[": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "*": "vim::MoveToNext",
+      "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+      // "g" commands
+      "g g": "vim::StartOfDocument",
+      "g h": "editor::Hover",
+      "g t": "pane::ActivateNextItem",
+      "g shift-t": "pane::ActivatePrevItem",
+      "g d": "editor::GoToDefinition",
+      "g shift-d": "editor::GoToTypeDefinition",
+      "g .": "editor::ToggleCodeActions", // zed specific
+      "g shift-a": "editor::FindAllReferences", // zed specific
+      "g *": [
+        "vim::MoveToNext",
+        {
+          "partialWord": true
+        }
+      ],
+      "g #": [
+        "vim::MoveToPrev",
+        {
+          "partialWord": true
+        }
+      ],
+      // z commands
+      "z t": "editor::ScrollCursorTop",
+      "z z": "editor::ScrollCursorCenter",
+      "z b": "editor::ScrollCursorBottom",
+      // Count support
       "1": [
         "vim::Number",
         1
@@ -135,7 +165,75 @@
       "9": [
         "vim::Number",
         9
-      ]
+      ],
+      // window related commands (ctrl-w X)
+      "ctrl-w left": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w right": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w up": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w down": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w h": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w l": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w k": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w j": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w ctrl-h": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w ctrl-l": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w ctrl-k": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w ctrl-j": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w g t": "pane::ActivateNextItem",
+      "ctrl-w ctrl-g t": "pane::ActivateNextItem",
+      "ctrl-w g shift-t": "pane::ActivatePrevItem",
+      "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
+      "ctrl-w w": "workspace::ActivateNextPane",
+      "ctrl-w ctrl-w": "workspace::ActivateNextPane",
+      "ctrl-w p": "workspace::ActivatePreviousPane",
+      "ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
+      "ctrl-w shift-w": "workspace::ActivatePreviousPane",
+      "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
+      "ctrl-w v": "pane::SplitLeft",
+      "ctrl-w ctrl-v": "pane::SplitLeft",
+      "ctrl-w s": "pane::SplitUp",
+      "ctrl-w shift-s": "pane::SplitUp",
+      "ctrl-w ctrl-s": "pane::SplitUp",
+      "ctrl-w c": "pane::CloseAllItems",
+      "ctrl-w ctrl-c": "pane::CloseAllItems",
+      "ctrl-w q": "pane::CloseAllItems",
+      "ctrl-w ctrl-q": "pane::CloseAllItems"
     }
   },
   {
@@ -156,12 +254,6 @@
         "vim::PushOperator",
         "Yank"
       ],
-      "z": [
-        "vim::PushOperator",
-        {
-          "Namespace": "Z"
-        }
-      ],
       "i": [
         "vim::SwitchMode",
         "Insert"
@@ -193,10 +285,18 @@
       "p": "vim::Paste",
       "u": "editor::Undo",
       "ctrl-r": "editor::Redo",
-      "/": [
-        "buffer_search::Deploy",
+      "/": "vim::Search",
+      "?": [
+        "vim::Search",
+        {
+          "backwards": true
+        }
+      ],
+      ";": "vim::RepeatFind",
+      ",": [
+        "vim::RepeatFind",
         {
-          "focus": true
+          "backwards": true
         }
       ],
       "ctrl-f": "vim::PageDown",
@@ -227,24 +327,11 @@
       ]
     }
   },
-  {
-    "context": "Editor && vim_operator == g",
-    "bindings": {
-      "g": "vim::StartOfDocument",
-      "h": "editor::Hover",
-      "t": "pane::ActivateNextItem",
-      "shift-t": "pane::ActivatePrevItem",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
-      "d": "editor::GoToDefinition"
-    }
-  },
   {
     "context": "Editor && vim_operator == c",
     "bindings": {
-      "c": "vim::CurrentLine"
+      "c": "vim::CurrentLine",
+      "d": "editor::Rename" // zed specific
     }
   },
   {
@@ -259,18 +346,6 @@
       "y": "vim::CurrentLine"
     }
   },
-  {
-    "context": "Editor && vim_operator == z",
-    "bindings": {
-      "t": "editor::ScrollCursorTop",
-      "z": "editor::ScrollCursorCenter",
-      "b": "editor::ScrollCursorBottom",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ]
-    }
-  },
   {
     "context": "Editor && VimObject",
     "bindings": {
@@ -314,15 +389,16 @@
         "vim::SwitchMode",
         "Normal"
       ],
-      "> >": "editor::Indent",
-      "< <": "editor::Outdent"
+      ">": "editor::Indent",
+      "<": "editor::Outdent"
     }
   },
   {
     "context": "Editor && vim_mode == insert",
     "bindings": {
       "escape": "vim::NormalBefore",
-      "ctrl-c": "vim::NormalBefore"
+      "ctrl-c": "vim::NormalBefore",
+      "ctrl-[": "vim::NormalBefore"
     }
   },
   {
@@ -333,7 +409,18 @@
       "escape": [
         "vim::SwitchMode",
         "Normal"
+      ],
+      "ctrl+[": [
+        "vim::SwitchMode",
+        "Normal"
       ]
     }
+  },
+  {
+    "context": "BufferSearchBar > VimEnabled",
+    "bindings": {
+      "enter": "vim::SearchSubmit",
+      "escape": "buffer_search::Dismiss"
+    }
   }
 ]

assets/settings/default.json 🔗

@@ -50,6 +50,13 @@
   // Whether to pop the completions menu while typing in an editor without
   // explicitly requesting it.
   "show_completions_on_input": true,
+  // Whether to show wrap guides in the editor. Setting this to true will
+  // show a guide at the 'preferred_line_length' value if softwrap is set to
+  // 'preferred_line_length', and will show any additional guides as specified
+  // by the 'wrap_guides' setting.
+  "show_wrap_guides": true,
+  // Character counts at which to show wrap guides in the editor.
+  "wrap_guides": [],
   // Whether to use additional LSP queries to format (and amend) the code after
   // every "trigger" symbol input, defined by LSP server capabilities.
   "use_on_type_format": true,
@@ -66,6 +73,11 @@
   // 3. Draw all invisible symbols:
   //   "all"
   "show_whitespaces": "selection",
+  // Settings related to calls in Zed
+  "calls": {
+    // Join calls with the microphone muted by default
+    "mute_on_join": true
+  },
   // Scrollbar related settings
   "scrollbar": {
     // When to show the scrollbar in the editor.
@@ -97,12 +109,18 @@
     "show_other_hints": true
   },
   "project_panel": {
-    // Whether to show the git status in the project panel.
-    "git_status": true,
+    // Default width of the project panel.
+    "default_width": 240,
     // Where to dock project panel. Can be 'left' or 'right'.
     "dock": "left",
-    // Default width of the project panel.
-    "default_width": 240
+    // Whether to show file icons in the project panel.
+    "file_icons": true,
+    // Whether to show folder icons or chevrons for directories in the project panel.
+    "folder_icons": true,
+    // Whether to show the git status in the project panel.
+    "git_status": true,
+    // Amount of indentation for nested items.
+    "indent_size": 20
   },
   "assistant": {
     // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
@@ -128,6 +146,13 @@
   // 4. Save when idle for a certain amount of time:
   //     "autosave": { "after_delay": {"milliseconds": 500} },
   "autosave": "off",
+  // Settings related to the editor's tabs
+  "tabs": {
+    // Show git status colors in the editor tabs.
+    "git_status": false,
+    // Position of the close button on the editor tabs.
+    "close_position": "right"
+  },
   // Whether or not to remove any trailing whitespace from lines of a buffer
   // before saving it.
   "remove_trailing_whitespace_on_save": true,
@@ -189,9 +214,7 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [
-      ".env"
-    ]
+    "disabled_globs": [".env"]
   },
   // Settings specific to journaling
   "journal": {
@@ -340,12 +363,6 @@
   // LSP Specific settings.
   "lsp": {
     // Specify the LSP name as a key here.
-    // As of 8/10/22, supported LSPs are:
-    // pyright
-    // gopls
-    // rust-analyzer
-    // typescript-language-server
-    // vscode-json-languageserver
     // "rust-analyzer": {
     //     //These initialization options are merged into Zed's defaults
     //     "initialization_options": {

crates/ai/src/assistant.rs 🔗

@@ -298,12 +298,22 @@ impl AssistantPanel {
     }
 
     fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
+        let mut propagate_action = true;
         if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
-                return;
-            }
+            search_bar.update(cx, |search_bar, cx| {
+                if search_bar.show(cx) {
+                    search_bar.search_suggested(cx);
+                    if action.focus {
+                        search_bar.select_query(cx);
+                        cx.focus_self();
+                    }
+                    propagate_action = false
+                }
+            });
+        }
+        if propagate_action {
+            cx.propagate_action();
         }
-        cx.propagate_action();
     }
 
     fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
@@ -320,13 +330,13 @@ impl AssistantPanel {
 
     fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
         if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
-            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx));
+            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
         }
     }
 
     fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
         if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
-            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx));
+            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
         }
     }
 

crates/call/Cargo.toml 🔗

@@ -36,6 +36,10 @@ anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true
 postage.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_derive.workspace = true
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/call/src/call.rs 🔗

@@ -1,9 +1,11 @@
+pub mod call_settings;
 pub mod participant;
 pub mod room;
 
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
+use call_settings::CallSettings;
 use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
@@ -19,6 +21,8 @@ pub use participant::ParticipantLocation;
 pub use room::Room;
 
 pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+    settings::register::<CallSettings>(cx);
+
     let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
     cx.set_global(active_call);
 }
@@ -280,21 +284,6 @@ impl ActiveCall {
         }
     }
 
-    pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
-        if let Some(room) = self.room().cloned() {
-            let toggle_screen_sharing = room.update(cx, |room, cx| {
-                if room.is_screen_sharing() {
-                    self.report_call_event("disable screen share", cx);
-                    Task::ready(room.unshare_screen(cx))
-                } else {
-                    self.report_call_event("enable screen share", cx);
-                    room.share_screen(cx)
-                }
-            });
-            toggle_screen_sharing.detach_and_log_err(cx);
-        }
-    }
-
     pub fn share_project(
         &mut self,
         project: ModelHandle<Project>,

crates/call/src/call_settings.rs 🔗

@@ -0,0 +1,27 @@
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize, Debug)]
+pub struct CallSettings {
+    pub mute_on_join: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct CallSettingsContent {
+    pub mute_on_join: Option<bool>,
+}
+
+impl Setting for CallSettings {
+    const KEY: Option<&'static str> = Some("calls");
+
+    type FileContent = CallSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/call/src/room.rs 🔗

@@ -1,4 +1,5 @@
 use crate::{
+    call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
     IncomingCall,
 };
@@ -153,8 +154,10 @@ impl Room {
             cx.spawn(|this, mut cx| async move {
                 connect.await?;
 
-                this.update(&mut cx, |this, cx| this.share_microphone(cx))
-                    .await?;
+                if !cx.read(|cx| settings::get::<CallSettings>(cx).mute_on_join) {
+                    this.update(&mut cx, |this, cx| this.share_microphone(cx))
+                        .await?;
+                }
 
                 anyhow::Ok(())
             })
@@ -656,7 +659,7 @@ impl Room {
                                     peer_id,
                                     projects: participant.projects,
                                     location,
-                                    muted: false,
+                                    muted: true,
                                     speaking: false,
                                     video_tracks: Default::default(),
                                     audio_tracks: Default::default(),
@@ -670,6 +673,10 @@ impl Room {
                                     live_kit.room.remote_video_tracks(&user.id.to_string());
                                 let audio_tracks =
                                     live_kit.room.remote_audio_tracks(&user.id.to_string());
+                                let publications = live_kit
+                                    .room
+                                    .remote_audio_track_publications(&user.id.to_string());
+
                                 for track in video_tracks {
                                     this.remote_video_track_updated(
                                         RemoteVideoTrackUpdate::Subscribed(track),
@@ -677,9 +684,15 @@ impl Room {
                                     )
                                     .log_err();
                                 }
-                                for track in audio_tracks {
+
+                                for (track, publication) in
+                                    audio_tracks.iter().zip(publications.iter())
+                                {
                                     this.remote_audio_track_updated(
-                                        RemoteAudioTrackUpdate::Subscribed(track),
+                                        RemoteAudioTrackUpdate::Subscribed(
+                                            track.clone(),
+                                            publication.clone(),
+                                        ),
                                         cx,
                                     )
                                     .log_err();
@@ -819,8 +832,8 @@ impl Room {
                 cx.notify();
             }
             RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
+                let mut found = false;
                 for participant in &mut self.remote_participants.values_mut() {
-                    let mut found = false;
                     for track in participant.audio_tracks.values() {
                         if track.sid() == track_id {
                             found = true;
@@ -832,16 +845,20 @@ impl Room {
                         break;
                     }
                 }
+
                 cx.notify();
             }
-            RemoteAudioTrackUpdate::Subscribed(track) => {
+            RemoteAudioTrackUpdate::Subscribed(track, publication) => {
                 let user_id = track.publisher_id().parse()?;
                 let track_id = track.sid().to_string();
                 let participant = self
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+
                 participant.audio_tracks.insert(track_id.clone(), track);
+                participant.muted = publication.is_muted();
+
                 cx.emit(Event::RemoteAudioTracksChanged {
                     participant_id: participant.peer_id,
                 });
@@ -1053,7 +1070,7 @@ impl Room {
         self.live_kit
             .as_ref()
             .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => None,
+                LocalTrack::None => Some(true),
                 LocalTrack::Pending { muted, .. } => Some(*muted),
                 LocalTrack::Published { muted, .. } => Some(*muted),
             })
@@ -1070,6 +1087,7 @@ impl Room {
         self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
     }
 
+    #[track_caller]
     pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         if self.status.is_offline() {
             return Task::ready(Err(anyhow!("room is offline")));
@@ -1244,6 +1262,10 @@ impl Room {
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
         let should_mute = !self.is_muted();
         if let Some(live_kit) = self.live_kit.as_mut() {
+            if matches!(live_kit.microphone_track, LocalTrack::None) {
+                return Ok(self.share_microphone(cx));
+            }
+
             let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
             live_kit.muted_by_user = should_mute;
 

crates/client/src/telemetry.rs 🔗

@@ -40,6 +40,7 @@ lazy_static! {
 struct ClickhouseEventRequestBody {
     token: &'static str,
     installation_id: Option<Arc<str>>,
+    is_staff: Option<bool>,
     app_version: Option<Arc<str>>,
     os_name: &'static str,
     os_version: Option<Arc<str>>,
@@ -224,6 +225,7 @@ impl Telemetry {
                             &ClickhouseEventRequestBody {
                                 token: ZED_SECRET_CLIENT_TOKEN,
                                 installation_id: state.installation_id.clone(),
+                                is_staff: state.is_staff.clone(),
                                 app_version: state.app_version.clone(),
                                 os_name: state.os_name,
                                 os_version: state.os_version.clone(),

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

@@ -7217,7 +7217,7 @@ async fn test_peers_following_each_other(
 
     // Clients A and B follow each other in split panes
     workspace_a.update(cx_a, |workspace, cx| {
-        workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
     });
     workspace_a
         .update(cx_a, |workspace, cx| {
@@ -7228,7 +7228,7 @@ async fn test_peers_following_each_other(
         .await
         .unwrap();
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
     });
     workspace_b
         .update(cx_b, |workspace, cx| {
@@ -7455,7 +7455,7 @@ async fn test_auto_unfollowing(
 
     // When client B activates a different pane, it continues following client A in the original pane.
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
+        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
     });
     assert_eq!(
         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -15,8 +15,8 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
     json::{self, ToJson},
     platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, ImageData, LayoutContext, ModelHandle, PaintContext, SceneBuilder,
+    Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use picker::PickerEvent;
 use project::{Project, RepositoryEntry};
@@ -652,10 +652,10 @@ impl CollabTitlebarItem {
         let is_muted = room.read(cx).is_muted();
         if is_muted {
             icon = "icons/radix/mic-mute.svg";
-            tooltip = "Unmute microphone\nRight click for options";
+            tooltip = "Unmute microphone";
         } else {
             icon = "icons/radix/mic.svg";
-            tooltip = "Mute microphone\nRight click for options";
+            tooltip = "Mute microphone";
         }
 
         let titlebar = &theme.titlebar;
@@ -705,10 +705,10 @@ impl CollabTitlebarItem {
         let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
         if is_deafened {
             icon = "icons/radix/speaker-off.svg";
-            tooltip = "Unmute speakers\nRight click for options";
+            tooltip = "Unmute speakers";
         } else {
             icon = "icons/radix/speaker-loud.svg";
-            tooltip = "Mute speakers\nRight click for options";
+            tooltip = "Mute speakers";
         }
 
         let titlebar = &theme.titlebar;
@@ -1312,7 +1312,7 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut CollabTitlebarItem,
-        _: &mut ViewContext<CollabTitlebarItem>,
+        _: &mut PaintContext<CollabTitlebarItem>,
     ) -> Self::PaintState {
         let mut path = PathBuilder::new();
         path.reset(bounds.lower_left());

crates/collab_ui/src/collab_ui.rs 🔗

@@ -18,13 +18,7 @@ use workspace::AppState;
 
 actions!(
     collab,
-    [
-        ToggleScreenSharing,
-        ToggleMute,
-        ToggleDeafen,
-        LeaveCall,
-        ShareMicrophone
-    ]
+    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -40,7 +34,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     cx.add_global_action(toggle_screen_sharing);
     cx.add_global_action(toggle_mute);
     cx.add_global_action(toggle_deafen);
-    cx.add_global_action(share_microphone);
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -71,10 +64,24 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
 }
 
 pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::toggle_mute)
-            .map(|task| task.detach_and_log_err(cx))
-            .log_err();
+    let call = ActiveCall::global(cx).read(cx);
+    if let Some(room) = call.room().cloned() {
+        let client = call.client();
+        room.update(cx, |room, cx| {
+            if room.is_muted() {
+                ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
+            } else {
+                ActiveCall::report_call_event_for_room(
+                    "disable microphone",
+                    room.id(),
+                    &client,
+                    cx,
+                );
+            }
+            room.toggle_mute(cx)
+        })
+        .map(|task| task.detach_and_log_err(cx))
+        .log_err();
     }
 }
 
@@ -85,10 +92,3 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
             .log_err();
     }
 }
-
-pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::share_microphone)
-            .detach_and_log_err(cx)
-    }
-}

crates/collab_ui/src/contact_finder.rs 🔗

@@ -67,7 +67,7 @@ impl PickerDelegate for ContactFinderDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(user) = self.potential_contacts.get(self.selected_index) {
             let user_store = self.user_store.read(cx);
             match user_store.contact_request_status(user) {

crates/collab_ui/src/face_pile.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     },
     json::ToJson,
     serde_json::{self, json},
-    AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
+    AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext,
 };
 
 use crate::CollabTitlebarItem;
@@ -54,7 +54,7 @@ impl Element<CollabTitlebarItem> for FacePile {
         visible_bounds: RectF,
         _layout: &mut Self::LayoutState,
         view: &mut CollabTitlebarItem,
-        cx: &mut ViewContext<CollabTitlebarItem>,
+        cx: &mut PaintContext<CollabTitlebarItem>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 

crates/command_palette/src/command_palette.rs 🔗

@@ -160,7 +160,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if !self.matches.is_empty() {
             let window_id = cx.window_id();
             let focused_view_id = self.focused_view_id;

crates/db/src/db.rs 🔗

@@ -7,7 +7,6 @@ use anyhow::Context;
 use gpui::AppContext;
 pub use indoc::indoc;
 pub use lazy_static;
-use parking_lot::{Mutex, RwLock};
 pub use smol;
 pub use sqlez;
 pub use sqlez_macros;
@@ -17,11 +16,9 @@ pub use util::paths::DB_DIR;
 use sqlez::domain::Migrator;
 use sqlez::thread_safe_connection::ThreadSafeConnection;
 use sqlez_macros::sql;
-use std::fs::create_dir_all;
 use std::future::Future;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
 use util::channel::ReleaseChannel;
 use util::{async_iife, ResultExt};
 
@@ -42,10 +39,8 @@ const DB_FILE_NAME: &'static str = "db.sqlite";
 
 lazy_static::lazy_static! {
     pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
-    pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
     pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
 }
-static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
 
 /// Open or create a database at the given directory path.
 /// This will retry a couple times if there are failures. If opening fails once, the db directory
@@ -63,66 +58,14 @@ pub async fn open_db<M: Migrator + 'static>(
     let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
 
     let connection = async_iife!({
-        // Note: This still has a race condition where 1 set of migrations succeeds
-        // (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
-        // This will cause the first connection to have the database taken out
-        // from under it. This *should* be fine though. The second dabatase failure will
-        // cause errors in the log and so should be observed by developers while writing
-        // soon-to-be good migrations. If user databases are corrupted, we toss them out
-        // and try again from a blank. As long as running all migrations from start to end
-        // on a blank database is ok, this race condition will never be triggered.
-        //
-        // Basically: Don't ever push invalid migrations to stable or everyone will have
-        // a bad time.
-
-        // If no db folder, create one at 0-{channel}
-        create_dir_all(&main_db_dir).context("Could not create db directory")?;
+        smol::fs::create_dir_all(&main_db_dir)
+            .await
+            .context("Could not create db directory")
+            .log_err()?;
         let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
-
-        // Optimistically open databases in parallel
-        if !DB_FILE_OPERATIONS.is_locked() {
-            // Try building a connection
-            if let Some(connection) = open_main_db(&db_path).await {
-                return Ok(connection)
-            };
-        }
-
-        // Take a lock in the failure case so that we move the db once per process instead
-        // of potentially multiple times from different threads. This shouldn't happen in the
-        // normal path
-        let _lock = DB_FILE_OPERATIONS.lock();
-        if let Some(connection) = open_main_db(&db_path).await {
-            return Ok(connection)
-        };
-
-        let backup_timestamp = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
-            .as_millis();
-
-        // If failed, move 0-{channel} to {current unix timestamp}-{channel}
-        let backup_db_dir = db_dir.join(Path::new(&format!(
-            "{}-{}",
-            backup_timestamp,
-            release_channel_name,
-        )));
-
-        std::fs::rename(&main_db_dir, &backup_db_dir)
-            .context("Failed clean up corrupted database, panicking.")?;
-
-        // Set a static ref with the failed timestamp and error so we can notify the user
-        {
-            let mut guard = BACKUP_DB_PATH.write();
-            *guard = Some(backup_db_dir);
-        }
-
-        // Create a new 0-{channel}
-        create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
-        let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
-
-        // Try again
-        open_main_db(&db_path).await.context("Could not newly created db")
-    }).await.log_err();
+        open_main_db(&db_path).await
+    })
+    .await;
 
     if let Some(connection) = connection {
         return connection;
@@ -249,13 +192,13 @@ where
 
 #[cfg(test)]
 mod tests {
-    use std::{fs, thread};
+    use std::thread;
 
-    use sqlez::{connection::Connection, domain::Domain};
+    use sqlez::domain::Domain;
     use sqlez_macros::sql;
     use tempdir::TempDir;
 
-    use crate::{open_db, DB_FILE_NAME};
+    use crate::open_db;
 
     // Test bad migration panics
     #[gpui::test]
@@ -321,31 +264,10 @@ mod tests {
                 .unwrap()
                 .is_none()
         );
-
-        let mut corrupted_backup_dir = fs::read_dir(tempdir.path())
-            .unwrap()
-            .find(|entry| {
-                !entry
-                    .as_ref()
-                    .unwrap()
-                    .file_name()
-                    .to_str()
-                    .unwrap()
-                    .starts_with("0")
-            })
-            .unwrap()
-            .unwrap()
-            .path();
-        corrupted_backup_dir.push(DB_FILE_NAME);
-
-        let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
-        assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
-            .unwrap()
-            .is_none());
     }
 
     /// Test that DB exists but corrupted (causing recreate)
-    #[gpui::test]
+    #[gpui::test(iterations = 30)]
     async fn test_simultaneous_db_corruption() {
         enum CorruptedDB {}
 

crates/editor/Cargo.toml 🔗

@@ -10,7 +10,6 @@ doctest = false
 
 [features]
 test-support = [
-    "rand",
     "copilot/test-support",
     "text/test-support",
     "language/test-support",
@@ -57,16 +56,16 @@ ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 pulldown-cmark = { version = "0.9.2", default-features = false }
-rand = { workspace = true, optional = true }
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-tree-sitter-rust = { version = "*", optional = true }
-tree-sitter-html = { version = "*", optional = true }
-tree-sitter-javascript = { version = "*", optional = true }
-tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
+rand.workspace = true
+
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-html = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
 
 [dev-dependencies]
 copilot = { path = "../copilot", features = ["test-support"] }
@@ -84,7 +83,6 @@ env_logger.workspace = true
 rand.workspace = true
 unindent.workspace = true
 tree-sitter.workspace = true
-tree-sitter-rust = "0.20"
-tree-sitter-html = "0.19"
-tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
-tree-sitter-javascript = "0.20"
+tree-sitter-rust.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-typescript.workspace = true

crates/editor/src/editor.rs 🔗

@@ -74,6 +74,7 @@ pub use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
+use rand::{seq::SliceRandom, thread_rng};
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -226,6 +227,10 @@ actions!(
         MoveLineUp,
         MoveLineDown,
         JoinLines,
+        SortLinesCaseSensitive,
+        SortLinesCaseInsensitive,
+        ReverseLines,
+        ShuffleLines,
         Transpose,
         Cut,
         Copy,
@@ -271,7 +276,9 @@ actions!(
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
         GoToDefinition,
+        GoToDefinitionSplit,
         GoToTypeDefinition,
+        GoToTypeDefinitionSplit,
         MoveToEnclosingBracket,
         UndoSelection,
         RedoSelection,
@@ -342,6 +349,10 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::outdent);
     cx.add_action(Editor::delete_line);
     cx.add_action(Editor::join_lines);
+    cx.add_action(Editor::sort_lines_case_sensitive);
+    cx.add_action(Editor::sort_lines_case_insensitive);
+    cx.add_action(Editor::reverse_lines);
+    cx.add_action(Editor::shuffle_lines);
     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);
@@ -407,7 +418,9 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::go_to_hunk);
     cx.add_action(Editor::go_to_prev_hunk);
     cx.add_action(Editor::go_to_definition);
+    cx.add_action(Editor::go_to_definition_split);
     cx.add_action(Editor::go_to_type_definition);
+    cx.add_action(Editor::go_to_type_definition_split);
     cx.add_action(Editor::fold);
     cx.add_action(Editor::fold_at);
     cx.add_action(Editor::unfold_lines);
@@ -545,6 +558,7 @@ pub struct Editor {
     pending_rename: Option<RenameState>,
     searchable: bool,
     cursor_shape: CursorShape,
+    collapse_matches: bool,
     workspace: Option<(WeakViewHandle<Workspace>, i64)>,
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
@@ -558,6 +572,7 @@ pub struct Editor {
     inlay_hint_cache: InlayHintCache,
     next_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
+    pixel_position_of_newest_cursor: Option<Vector2F>,
 }
 
 pub struct EditorSnapshot {
@@ -1377,6 +1392,7 @@ impl Editor {
             searchable: true,
             override_text_style: None,
             cursor_shape: Default::default(),
+            collapse_matches: false,
             workspace: None,
             keymap_context_layers: Default::default(),
             input_enabled: true,
@@ -1388,6 +1404,7 @@ impl Editor {
             copilot_state: Default::default(),
             inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
             gutter_hovered: false,
+            pixel_position_of_newest_cursor: None,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1516,6 +1533,17 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
+        self.collapse_matches = collapse_matches;
+    }
+
+    fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
+        if self.collapse_matches {
+            return range.start..range.start;
+        }
+        range.clone()
+    }
+
     pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
         if self.display_map.read(cx).clip_at_line_ends != clip {
             self.display_map
@@ -2655,11 +2683,16 @@ impl Editor {
             InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
         };
 
-        self.inlay_hint_cache.refresh_inlay_hints(
+        if let Some(InlaySplice {
+            to_remove,
+            to_insert,
+        }) = self.inlay_hint_cache.spawn_hint_refresh(
             self.excerpt_visible_offsets(required_languages.as_ref(), cx),
             invalidate_cache,
             cx,
-        )
+        ) {
+            self.splice_inlay_hints(to_remove, to_insert, cx);
+        }
     }
 
     fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
@@ -4181,6 +4214,96 @@ impl Editor {
         });
     }
 
+    pub fn sort_lines_case_sensitive(
+        &mut self,
+        _: &SortLinesCaseSensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, |text| text.sort())
+    }
+
+    pub fn sort_lines_case_insensitive(
+        &mut self,
+        _: &SortLinesCaseInsensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase()))
+    }
+
+    pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
+        self.manipulate_lines(cx, |lines| lines.reverse())
+    }
+
+    pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
+        self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng()))
+    }
+
+    fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(&mut [&str]),
+    {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut edits = Vec::new();
+
+        let selections = self.selections.all::<Point>(cx);
+        let mut selections = selections.iter().peekable();
+        let mut contiguous_row_selections = Vec::new();
+        let mut new_selections = Vec::new();
+
+        while let Some(selection) = selections.next() {
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
+
+            let start_point = Point::new(start_row, 0);
+            let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1));
+            let text = buffer
+                .text_for_range(start_point..end_point)
+                .collect::<String>();
+            let mut text = text.split("\n").collect_vec();
+
+            let text_len = text.len();
+            callback(&mut text);
+
+            // 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,
+                "callback should not change the number of lines"
+            );
+
+            edits.push((start_point..end_point, text.join("\n")));
+            let start_anchor = buffer.anchor_after(start_point);
+            let end_anchor = buffer.anchor_before(end_point);
+
+            // Make selection and push
+            new_selections.push(Selection {
+                id: selection.id,
+                start: start_anchor.to_offset(&buffer),
+                end: end_anchor.to_offset(&buffer),
+                goal: SelectionGoal::None,
+                reversed: selection.reversed,
+            });
+        }
+
+        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;
@@ -5274,7 +5397,7 @@ impl Editor {
 
     pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
         let end = self.buffer.read(cx).read(cx).len();
-        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+        self.change_selections(None, cx, |s| {
             s.select_ranges(vec![0..end]);
         });
     }
@@ -6185,14 +6308,31 @@ impl Editor {
     }
 
     pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
-        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx);
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
     }
 
     pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
-        self.go_to_definition_of_kind(GotoDefinitionKind::Type, cx);
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
+    }
+
+    pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx);
+    }
+
+    pub fn go_to_type_definition_split(
+        &mut self,
+        _: &GoToTypeDefinitionSplit,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx);
     }
 
-    fn go_to_definition_of_kind(&mut self, kind: GotoDefinitionKind, cx: &mut ViewContext<Self>) {
+    fn go_to_definition_of_kind(
+        &mut self,
+        kind: GotoDefinitionKind,
+        split: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         let Some(workspace) = self.workspace(cx) else { return };
         let buffer = self.buffer.read(cx);
         let head = self.selections.newest::<usize>(cx).head();
@@ -6211,7 +6351,7 @@ impl Editor {
         cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
             let definitions = definitions.await?;
             editor.update(&mut cx, |editor, cx| {
-                editor.navigate_to_definitions(definitions, cx);
+                editor.navigate_to_definitions(definitions, split, cx);
             })?;
             Ok::<(), anyhow::Error>(())
         })
@@ -6221,6 +6361,7 @@ impl Editor {
     pub fn navigate_to_definitions(
         &mut self,
         mut definitions: Vec<LocationLink>,
+        split: bool,
         cx: &mut ViewContext<Editor>,
     ) {
         let Some(workspace) = self.workspace(cx) else { return };
@@ -6234,18 +6375,24 @@ impl Editor {
                 .to_offset(definition.target.buffer.read(cx));
 
             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]);
                 });
             } else {
                 cx.window_context().defer(move |cx| {
                     let target_editor: ViewHandle<Self> = workspace.update(cx, |workspace, cx| {
-                        workspace.open_project_item(definition.target.buffer.clone(), cx)
+                        if split {
+                            workspace.split_project_item(definition.target.buffer.clone(), cx)
+                        } else {
+                            workspace.open_project_item(definition.target.buffer.clone(), cx)
+                        }
                     });
                     target_editor.update(cx, |target_editor, cx| {
                         // 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]);
                         });
@@ -6276,7 +6423,9 @@ impl Editor {
                     .map(|definition| definition.target)
                     .collect();
                 workspace.update(cx, |workspace, cx| {
-                    Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, split, cx,
+                    )
                 });
             });
         }
@@ -6321,7 +6470,7 @@ impl Editor {
                         })
                         .unwrap();
                     Self::open_locations_in_multibuffer(
-                        workspace, locations, replica_id, title, cx,
+                        workspace, locations, replica_id, title, false, cx,
                     );
                 })?;
 
@@ -6336,6 +6485,7 @@ impl Editor {
         mut locations: Vec<Location>,
         replica_id: ReplicaId,
         title: String,
+        split: bool,
         cx: &mut ViewContext<Workspace>,
     ) {
         // If there are multiple definitions, open them in a multibuffer
@@ -6382,7 +6532,11 @@ impl Editor {
                 cx,
             );
         });
-        workspace.add_item(Box::new(editor), cx);
+        if split {
+            workspace.split_item(Box::new(editor), cx);
+        } else {
+            workspace.add_item(Box::new(editor), cx);
+        }
     }
 
     pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@@ -7031,6 +7185,20 @@ impl Editor {
             .text()
     }
 
+    pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
+        let mut wrap_guides = smallvec::smallvec![];
+
+        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) {
+                wrap_guides.push((soft_wrap as usize, true));
+            }
+            wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
+        }
+
+        wrap_guides
+    }
+
     pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
         let settings = self.buffer.read(cx).settings_at(0, cx);
         let mode = self

crates/editor/src/editor_tests.rs 🔗

@@ -2500,6 +2500,156 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test sort_lines_case_insensitive()
+    cx.set_state(indoc! {"
+        «z
+        y
+        x
+        Z
+        Y
+        Xˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «x
+        X
+        y
+        Y
+        z
+        Zˇ»
+    "});
+
+    // Test reverse_lines()
+    cx.set_state(indoc! {"
+        «5
+        4
+        3
+        2
+        1ˇ»
+    "});
+    cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
+    cx.assert_editor_state(indoc! {"
+        «1
+        2
+        3
+        4
+        5ˇ»
+    "});
+
+    // Skip testing shuffle_line()
+
+    // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
+    // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
+
+    // Don't manipulate when cursor is on single line, but expand the selection
+    cx.set_state(indoc! {"
+        ddˇdd
+        ccc
+        bb
+        a
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «ddddˇ»
+        ccc
+        bb
+        a
+    "});
+
+    // Basic manipulate case
+    // Start selection moves to column 0
+    // End of selection shrinks to fit shorter line
+    cx.set_state(indoc! {"
+        dd«d
+        ccc
+        bb
+        aaaaaˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaaa
+        bb
+        ccc
+        dddˇ»
+    "});
+
+    // Manipulate case with newlines
+    cx.set_state(indoc! {"
+        dd«d
+        ccc
+
+        bb
+        aaaaa
+
+        ˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «
+
+        aaaaa
+        bb
+        ccc
+        dddˇ»
+
+    "});
+}
+
+#[gpui::test]
+async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Manipulate with multiple selections on a single line
+    cx.set_state(indoc! {"
+        dd«dd
+        cˇ»c«c
+        bb
+        aaaˇ»aa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaaa
+        bb
+        ccc
+        ddddˇ»
+    "});
+
+    // Manipulate with multiple disjoin selections
+    cx.set_state(indoc! {"
+        5«
+        4
+        3
+        2
+        1ˇ»
+
+        dd«dd
+        ccc
+        bb
+        aaaˇ»aa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «1
+        2
+        3
+        4
+        5ˇ»
+
+        «aaaaa
+        bb
+        ccc
+        ddddˇ»
+    "});
+}
+
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -3836,7 +3986,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
             autoclose_before: "})]>".into(),
             ..Default::default()
         },
-        Some(tree_sitter_javascript::language()),
+        Some(tree_sitter_typescript::language_tsx()),
     ));
 
     let registry = Arc::new(LanguageRegistry::test());
@@ -5383,7 +5533,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
             line_comment: Some("// ".into()),
             ..Default::default()
         },
-        Some(tree_sitter_javascript::language()),
+        Some(tree_sitter_typescript::language_tsx()),
     ));
 
     let registry = Arc::new(LanguageRegistry::test());

crates/editor/src/element.rs 🔗

@@ -32,7 +32,7 @@ use gpui::{
     platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
     text_layout::{self, Line, RunStyle, TextLayoutCache},
     AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
-    MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
+    MouseRegion, PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use json::json;
@@ -61,6 +61,7 @@ enum FoldMarkers {}
 struct SelectionLayout {
     head: DisplayPoint,
     cursor_shape: CursorShape,
+    is_newest: bool,
     range: Range<DisplayPoint>,
 }
 
@@ -70,6 +71,7 @@ impl SelectionLayout {
         line_mode: bool,
         cursor_shape: CursorShape,
         map: &DisplaySnapshot,
+        is_newest: bool,
     ) -> Self {
         if line_mode {
             let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
@@ -77,6 +79,7 @@ impl SelectionLayout {
             Self {
                 head: selection.head().to_display_point(map),
                 cursor_shape,
+                is_newest,
                 range: point_range.start.to_display_point(map)
                     ..point_range.end.to_display_point(map),
             }
@@ -85,6 +88,7 @@ impl SelectionLayout {
             Self {
                 head: selection.head(),
                 cursor_shape,
+                is_newest,
                 range: selection.range(),
             }
         }
@@ -156,6 +160,7 @@ impl EditorElement {
                         event.position,
                         event.cmd,
                         event.shift,
+                        event.alt,
                         position_map.as_ref(),
                         text_bounds,
                         cx,
@@ -308,6 +313,7 @@ impl EditorElement {
         position: Vector2F,
         cmd: bool,
         shift: bool,
+        alt: bool,
         position_map: &PositionMap,
         text_bounds: RectF,
         cx: &mut EventContext<Editor>,
@@ -324,9 +330,9 @@ impl EditorElement {
 
             if point == target_point {
                 if shift {
-                    go_to_fetched_type_definition(editor, point, cx);
+                    go_to_fetched_type_definition(editor, point, alt, cx);
                 } else {
-                    go_to_fetched_definition(editor, point, cx);
+                    go_to_fetched_definition(editor, point, alt, cx);
                 }
 
                 return true;
@@ -535,6 +541,24 @@ impl EditorElement {
                     corner_radius: 0.,
                 });
             }
+
+            for (wrap_position, active) in layout.wrap_guides.iter() {
+                let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.;
+                let color = if *active {
+                    self.style.active_wrap_guide
+                } else {
+                    self.style.wrap_guide
+                };
+                scene.push_quad(Quad {
+                    bounds: RectF::new(
+                        vec2f(x, text_bounds.origin_y()),
+                        vec2f(1., text_bounds.height()),
+                    ),
+                    background: Some(color),
+                    border: Border::new(0., Color::transparent_black()),
+                    corner_radius: 0.,
+                });
+            }
         }
     }
 
@@ -862,6 +886,12 @@ impl EditorElement {
                         let x = cursor_character_x - scroll_left;
                         let y = cursor_position.row() as f32 * layout.position_map.line_height
                             - scroll_top;
+                        if selection.is_newest {
+                            editor.pixel_position_of_newest_cursor = Some(vec2f(
+                                bounds.origin_x() + x + block_width / 2.,
+                                bounds.origin_y() + y + layout.position_map.line_height / 2.,
+                            ));
+                        }
                         cursors.push(Cursor {
                             color: selection_style.cursor,
                             block_width,
@@ -1308,16 +1338,15 @@ impl EditorElement {
         }
     }
 
-    fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
-        let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
+    fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> f32 {
         let style = &self.style;
 
         cx.text_layout_cache()
             .layout_str(
-                "1".repeat(digit_count).as_str(),
+                " ".repeat(column).as_str(),
                 style.text.font_size,
                 &[(
-                    digit_count,
+                    column,
                     RunStyle {
                         font_id: style.text.font_id,
                         color: Color::black(),
@@ -1328,6 +1357,11 @@ impl EditorElement {
             .width()
     }
 
+    fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
+        let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
+        self.column_pixels(digit_count, cx)
+    }
+
     //Folds contained in a hunk are ignored apart from shrinking visual size
     //If a fold contains any hunks then that fold line is marked as modified
     fn layout_git_gutters(
@@ -1975,6 +2009,7 @@ impl Element<Editor> for EditorElement {
 
         let snapshot = editor.snapshot(cx);
         let style = self.style.clone();
+
         let line_height = (style.text.font_size * style.line_height_scalar).round();
 
         let gutter_padding;
@@ -2012,6 +2047,12 @@ impl Element<Editor> for EditorElement {
             }
         };
 
+        let wrap_guides = editor
+            .wrap_guides(cx)
+            .iter()
+            .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+            .collect();
+
         let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
         if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
             size.set_y(
@@ -2106,6 +2147,7 @@ impl Element<Editor> for EditorElement {
                     line_mode,
                     cursor_shape,
                     &snapshot.display_snapshot,
+                    false,
                 ));
         }
         selections.extend(remote_selections);
@@ -2115,6 +2157,7 @@ impl Element<Editor> for EditorElement {
                 .selections
                 .disjoint_in_range(start_anchor..end_anchor, cx);
             local_selections.extend(editor.selections.pending(cx));
+            let newest = editor.selections.newest(cx);
             for selection in &local_selections {
                 let is_empty = selection.start == selection.end;
                 let selection_start = snapshot.prev_line_boundary(selection.start).1;
@@ -2137,11 +2180,13 @@ impl Element<Editor> for EditorElement {
                 local_selections
                     .into_iter()
                     .map(|selection| {
+                        let is_newest = selection == newest;
                         SelectionLayout::new(
                             selection,
                             editor.selections.line_mode,
                             editor.cursor_shape,
                             &snapshot.display_snapshot,
+                            is_newest,
                         )
                     })
                     .collect(),
@@ -2368,6 +2413,7 @@ impl Element<Editor> for EditorElement {
                     snapshot,
                 }),
                 visible_display_row_range: start_row..end_row,
+                wrap_guides,
                 gutter_size,
                 gutter_padding,
                 text_size,
@@ -2409,7 +2455,7 @@ impl Element<Editor> for EditorElement {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
+        cx: &mut PaintContext<Editor>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         scene.push_layer(Some(visible_bounds));
@@ -2518,6 +2564,7 @@ pub struct LayoutState {
     gutter_margin: f32,
     text_size: Vector2F,
     mode: EditorMode,
+    wrap_guides: SmallVec<[(f32, bool); 2]>,
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
@@ -3004,7 +3051,14 @@ mod tests {
         let mut scene = SceneBuilder::new(1.0);
         let bounds = RectF::new(Default::default(), size);
         editor.update(cx, |editor, cx| {
-            element.paint(&mut scene, bounds, bounds, &mut state, editor, cx);
+            element.paint(
+                &mut scene,
+                bounds,
+                bounds,
+                &mut state,
+                editor,
+                &mut PaintContext::new(cx),
+            );
         });
     }
 

crates/editor/src/hover_popover.rs 🔗

@@ -198,7 +198,7 @@ fn show_hover(
 
             // Construct new hover popover from hover request
             let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
-                if hover_result.contents.is_empty() {
+                if hover_result.is_empty() {
                     return None;
                 }
 
@@ -420,7 +420,7 @@ fn render_blocks(
 
     RenderedInfo {
         theme_id,
-        text,
+        text: text.trim().to_string(),
         highlights,
         region_ranges,
         regions,
@@ -816,6 +816,118 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            «fn» test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("regular text for hover to show".to_string()),
+                    lsp::MarkedString::String("".to_string()),
+                    lsp::MarkedString::LanguageString(lsp::LanguageString {
+                        language: "Rust".to_string(),
+                        value: "".to_string(),
+                    }),
+                ]),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "regular text for hover to show".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }],
+                "No empty string hovers should be shown"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            «fn» test() { println!(); }
+        "});
+
+        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
+        let markdown_string = format!("\n```rust\n{code_str}```");
+
+        let closure_markdown_string = markdown_string.clone();
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
+            let future_markdown_string = closure_markdown_string.clone();
+            async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: future_markdown_string,
+                    }),
+                    range: Some(symbol_range),
+                }))
+            }
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, cx| {
+            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
+            assert_eq!(
+                blocks,
+                vec![HoverBlock {
+                    text: markdown_string,
+                    kind: HoverBlockKind::Markdown,
+                }],
+            );
+
+            let style = editor.style(cx);
+            let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
+            assert_eq!(
+                rendered.text,
+                code_str.trim(),
+                "Should not have extra line breaks at end of rendered hover"
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -195,20 +195,41 @@ impl InlayHintCache {
         }
     }
 
-    pub fn refresh_inlay_hints(
+    pub fn spawn_hint_refresh(
         &mut self,
         mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
         invalidate: InvalidationStrategy,
         cx: &mut ViewContext<Editor>,
-    ) {
-        if !self.enabled || excerpts_to_query.is_empty() {
-            return;
+    ) -> Option<InlaySplice> {
+        if !self.enabled {
+            return None;
         }
+
         let update_tasks = &mut self.update_tasks;
+        let mut invalidated_hints = Vec::new();
         if invalidate.should_invalidate() {
-            update_tasks
-                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+            let mut changed = false;
+            update_tasks.retain(|task_excerpt_id, _| {
+                let retain = excerpts_to_query.contains_key(task_excerpt_id);
+                changed |= !retain;
+                retain
+            });
+            self.hints.retain(|cached_excerpt, cached_hints| {
+                let retain = excerpts_to_query.contains_key(cached_excerpt);
+                changed |= !retain;
+                if !retain {
+                    invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
+                }
+                retain
+            });
+            if changed {
+                self.version += 1;
+            }
         }
+        if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
+            return None;
+        }
+
         let cache_version = self.version;
         excerpts_to_query.retain(|visible_excerpt_id, _| {
             match update_tasks.entry(*visible_excerpt_id) {
@@ -229,6 +250,15 @@ impl InlayHintCache {
                 .ok();
         })
         .detach();
+
+        if invalidated_hints.is_empty() {
+            None
+        } else {
+            Some(InlaySplice {
+                to_remove: invalidated_hints,
+                to_insert: Vec::new(),
+            })
+        }
     }
 
     fn new_allowed_hint_kinds_splice(
@@ -684,7 +714,7 @@ async fn fetch_and_update_hints(
 
                 if query.invalidate.should_invalidate() {
                     let mut outdated_excerpt_caches = HashSet::default();
-                    for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
+                    for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
                         let excerpt_hints = excerpt_hints.read();
                         if excerpt_hints.buffer_id == query.buffer_id
                             && excerpt_id != &query.excerpt_id
@@ -1022,9 +1052,9 @@ mod tests {
                 "Should get its first hints when opening the editor"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, edits_made,
+                editor.inlay_hint_cache().version,
+                edits_made,
                 "The editor update the cache version after every cache/view change"
             );
         });
@@ -1053,9 +1083,9 @@ mod tests {
                 "Should not update hints while the work task is running"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, edits_made,
+                editor.inlay_hint_cache().version,
+                edits_made,
                 "Should not update the cache while the work task is running"
             );
         });
@@ -1077,9 +1107,9 @@ mod tests {
                 "New hints should be queried after the work task is done"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, edits_made,
+                editor.inlay_hint_cache().version,
+                edits_made,
                 "Cache version should udpate once after the work task is done"
             );
         });
@@ -1194,9 +1224,9 @@ mod tests {
                 "Should get its first hints when opening the editor"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, 1,
+                editor.inlay_hint_cache().version,
+                1,
                 "Rust editor update the cache version after every cache/view change"
             );
         });
@@ -1252,8 +1282,7 @@ mod tests {
                 "Markdown editor should have a separate verison, repeating Rust editor rules"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 1);
+            assert_eq!(editor.inlay_hint_cache().version, 1);
         });
 
         rs_editor.update(cx, |editor, cx| {
@@ -1269,9 +1298,9 @@ mod tests {
                 "Rust inlay cache should change after the edit"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, 2,
+                editor.inlay_hint_cache().version,
+                2,
                 "Every time hint cache changes, cache version should be incremented"
             );
         });
@@ -1283,8 +1312,7 @@ mod tests {
                 "Markdown editor should not be affected by Rust editor changes"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 1);
+            assert_eq!(editor.inlay_hint_cache().version, 1);
         });
 
         md_editor.update(cx, |editor, cx| {
@@ -1300,8 +1328,7 @@ mod tests {
                 "Rust editor should not be affected by Markdown editor changes"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 2);
+            assert_eq!(editor.inlay_hint_cache().version, 2);
         });
         rs_editor.update(cx, |editor, cx| {
             let expected_layers = vec!["1".to_string()];
@@ -1311,8 +1338,7 @@ mod tests {
                 "Markdown editor should also change independently"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 2);
+            assert_eq!(editor.inlay_hint_cache().version, 2);
         });
     }
 
@@ -1433,9 +1459,9 @@ mod tests {
                 vec!["other hint".to_string(), "type hint".to_string()],
                 visible_hint_labels(editor, cx)
             );
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, edits_made,
+                editor.inlay_hint_cache().version,
+                edits_made,
                 "Should not update cache version due to new loaded hints being the same"
             );
         });
@@ -1568,9 +1594,8 @@ mod tests {
             );
             assert!(cached_hint_labels(editor).is_empty());
             assert!(visible_hint_labels(editor, cx).is_empty());
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, edits_made,
+                editor.inlay_hint_cache().version, edits_made,
                 "The editor should not update the cache version after /refresh query without updates"
             );
         });
@@ -1641,8 +1666,7 @@ mod tests {
                 vec!["parameter hint".to_string()],
                 visible_hint_labels(editor, cx),
             );
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, edits_made);
+            assert_eq!(editor.inlay_hint_cache().version, edits_made);
         });
     }
 
@@ -1720,9 +1744,8 @@ mod tests {
                 "Should get hints from the last edit landed only"
             );
             assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, 1,
+                editor.inlay_hint_cache().version, 1,
                 "Only one update should be registered in the cache after all cancellations"
             );
         });
@@ -1766,9 +1789,9 @@ mod tests {
                 "Should get hints from the last edit landed only"
             );
             assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, 2,
+                editor.inlay_hint_cache().version,
+                2,
                 "Should update the cache version once more, for the new change"
             );
         });
@@ -1886,9 +1909,8 @@ mod tests {
                 "Should have hints from both LSP requests made for a big file"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
-                inlay_cache.version, 2,
+                editor.inlay_hint_cache().version, 2,
                 "Both LSP queries should've bumped the cache version"
             );
         });
@@ -1918,8 +1940,7 @@ mod tests {
             assert_eq!(expected_layers, cached_hint_labels(editor),
                 "Should have hints from the new LSP response after edit");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
+            assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
         });
     }
 
@@ -2075,6 +2096,7 @@ mod tests {
                         panic!("unexpected uri: {:?}", params.text_document.uri);
                     };
 
+                    // one hint per excerpt
                     let positions = [
                         lsp::Position::new(0, 2),
                         lsp::Position::new(4, 2),
@@ -2138,8 +2160,7 @@ mod tests {
                 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+            assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
         });
 
         editor.update(cx, |editor, cx| {
@@ -2169,8 +2190,8 @@ mod tests {
             assert_eq!(expected_layers, cached_hint_labels(editor),
                 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 9);
+            assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
+                "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
         });
 
         editor.update(cx, |editor, cx| {
@@ -2179,7 +2200,7 @@ mod tests {
             });
         });
         cx.foreground().run_until_parked();
-        editor.update(cx, |editor, cx| {
+        let last_scroll_update_version = editor.update(cx, |editor, cx| {
             let expected_layers = vec![
                 "main hint #0".to_string(),
                 "main hint #1".to_string(),
@@ -2197,8 +2218,8 @@ mod tests {
             assert_eq!(expected_layers, cached_hint_labels(editor),
                 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 12);
+            assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
+            expected_layers.len()
         });
 
         editor.update(cx, |editor, cx| {
@@ -2225,12 +2246,14 @@ mod tests {
             assert_eq!(expected_layers, cached_hint_labels(editor),
                 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
+            assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
         });
 
         editor_edited.store(true, Ordering::Release);
         editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+            });
             editor.handle_input("++++more text++++", cx);
         });
         cx.foreground().run_until_parked();
@@ -2240,19 +2263,253 @@ mod tests {
                 "main hint(edited) #1".to_string(),
                 "main hint(edited) #2".to_string(),
                 "main hint(edited) #3".to_string(),
-                "other hint #0".to_string(),
-                "other hint #1".to_string(),
-                "other hint #2".to_string(),
-                "other hint #3".to_string(),
-                "other hint #4".to_string(),
-                "other hint #5".to_string(),
+                "main hint(edited) #4".to_string(),
+                "main hint(edited) #5".to_string(),
+                "other hint(edited) #0".to_string(),
+                "other hint(edited) #1".to_string(),
             ];
-            assert_eq!(expected_layers, cached_hint_labels(editor),
-                "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
-unedited (2nd) buffer should have the same hint");
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "After multibuffer edit, editor gets scolled back to the last selection; \
+all hints should be invalidated and requeried for all of its visible excerpts"
+            );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.version, 16);
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                last_scroll_update_version + expected_layers.len() + 1,
+                "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_excerpts_removed(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: false,
+                show_parameter_hints: false,
+                show_other_hints: false,
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
+            let buffer_1_excerpts = multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(2, 0),
+                    primary: None,
+                }],
+                cx,
+            );
+            let buffer_2_excerpts = multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [ExcerptRange {
+                    context: Point::new(0, 1)..Point::new(2, 1),
+                    primary: None,
+                }],
+                cx,
+            );
+            (buffer_1_excerpts, buffer_2_excerpts)
+        });
+
+        assert!(!buffer_1_excerpts.is_empty());
+        assert!(!buffer_2_excerpts.is_empty());
+
+        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_edited = Arc::new(AtomicBool::new(false));
+        let fake_server = fake_servers.next().await.unwrap();
+        let closure_editor_edited = Arc::clone(&editor_edited);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_editor_edited = Arc::clone(&closure_editor_edited);
+                async move {
+                    let hint_text = if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                    {
+                        "main hint"
+                    } else if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                    {
+                        "other hint"
+                    } else {
+                        panic!("unexpected uri: {:?}", params.text_document.uri);
+                    };
+
+                    let positions = [
+                        lsp::Position::new(0, 2),
+                        lsp::Position::new(4, 2),
+                        lsp::Position::new(22, 2),
+                        lsp::Position::new(44, 2),
+                        lsp::Position::new(56, 2),
+                        lsp::Position::new(67, 2),
+                    ];
+                    let out_of_range_hint = lsp::InlayHint {
+                        position: lsp::Position::new(
+                            params.range.start.line + 99,
+                            params.range.start.character + 99,
+                        ),
+                        label: lsp::InlayHintLabel::String(
+                            "out of excerpt range, should be ignored".to_string(),
+                        ),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    };
+
+                    let edited = task_editor_edited.load(Ordering::Acquire);
+                    Ok(Some(
+                        std::iter::once(out_of_range_hint)
+                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                                lsp::InlayHint {
+                                    position,
+                                    label: lsp::InlayHintLabel::String(format!(
+                                        "{hint_text}{} #{i}",
+                                        if edited { "(edited)" } else { "" },
+                                    )),
+                                    kind: None,
+                                    text_edits: None,
+                                    tooltip: None,
+                                    padding_left: None,
+                                    padding_right: None,
+                                    data: None,
+                                }
+                            }))
+                            .collect(),
+                    ))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                vec!["main hint #0".to_string(), "other hint #0".to_string()],
+                cached_hint_labels(editor),
+                "Cache should update for both excerpts despite hints display was disabled"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "All hints are disabled and should not be shown despite being present in the cache"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                2,
+                "Cache should update once per excerpt query"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.buffer().update(cx, |multibuffer, cx| {
+                multibuffer.remove_excerpts(buffer_2_excerpts, cx)
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                vec!["main hint #0".to_string()],
+                cached_hint_labels(editor),
+                "For the removed excerpt, should clean corresponding cached hints"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "All hints are disabled and should not be shown despite being present in the cache"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                3,
+                "Excerpt removal should trigger cache update"
+            );
+        });
+
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["main hint #0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Hint display settings change should not change the cache"
+            );
+            assert_eq!(
+                expected_hints,
+                visible_hint_labels(editor, cx),
+                "Settings change should make cached hints visible"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                4,
+                "Settings change should trigger cache update"
+            );
         });
     }
 

crates/editor/src/items.rs 🔗

@@ -7,8 +7,10 @@ use anyhow::{Context, Result};
 use collections::HashSet;
 use futures::future::try_join_all;
 use gpui::{
-    elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
-    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    elements::*,
+    geometry::vector::{vec2f, Vector2F},
+    AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
 use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@@ -750,6 +752,10 @@ impl Item for Editor {
         Some(Box::new(handle.clone()))
     }
 
+    fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+        self.pixel_position_of_newest_cursor
+    }
+
     fn breadcrumb_location(&self) -> ToolbarItemLocation {
         ToolbarItemLocation::PrimaryLeft { flex: None }
     }
@@ -887,10 +893,20 @@ pub(crate) enum BufferSearchHighlights {}
 impl SearchableItem for Editor {
     type Match = Range<Anchor>;
 
-    fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        _: &mut ViewContext<Self>,
+    ) -> Option<SearchEvent> {
         match event {
             Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
-            Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged),
+            Event::SelectionsChanged { .. } => {
+                if self.selections.disjoint_anchors().len() == 1 {
+                    Some(SearchEvent::ActiveMatchChanged)
+                } else {
+                    None
+                }
+            }
             _ => None,
         }
     }
@@ -936,46 +952,68 @@ impl SearchableItem for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.unfold_ranges([matches[index].clone()], false, true, cx);
+        let range = self.range_for_match(&matches[index]);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-            s.select_ranges([matches[index].clone()])
-        });
+            s.select_ranges([range]);
+        })
+    }
+
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.unfold_ranges(matches.clone(), false, false, cx);
+        let mut ranges = Vec::new();
+        for m in &matches {
+            ranges.push(self.range_for_match(&m))
+        }
+        self.change_selections(None, cx, |s| s.select_ranges(ranges));
     }
 
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Range<Anchor>>,
-        mut current_index: usize,
+        current_index: usize,
         direction: Direction,
+        count: usize,
         cx: &mut ViewContext<Self>,
     ) -> usize {
         let buffer = self.buffer().read(cx).snapshot(cx);
-        let cursor = self.selections.newest_anchor().head();
-        if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
-            if direction == Direction::Prev {
-                if current_index == 0 {
-                    current_index = matches.len() - 1;
-                } else {
-                    current_index -= 1;
+        let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
+            self.selections.newest_anchor().head()
+        } else {
+            matches[current_index].start
+        };
+
+        let mut count = count % matches.len();
+        if count == 0 {
+            return current_index;
+        }
+        match direction {
+            Direction::Next => {
+                if matches[current_index]
+                    .start
+                    .cmp(&current_index_position, &buffer)
+                    .is_gt()
+                {
+                    count = count - 1
                 }
+
+                (current_index + count) % matches.len()
             }
-        } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
-            if direction == Direction::Next {
-                current_index = 0;
-            }
-        } else if direction == Direction::Prev {
-            if current_index == 0 {
-                current_index = matches.len() - 1;
-            } else {
-                current_index -= 1;
-            }
-        } else if direction == Direction::Next {
-            if current_index == matches.len() - 1 {
-                current_index = 0
-            } else {
-                current_index += 1;
+            Direction::Prev => {
+                if matches[current_index]
+                    .end
+                    .cmp(&current_index_position, &buffer)
+                    .is_lt()
+                {
+                    count = count - 1;
+                }
+
+                if current_index >= count {
+                    current_index - count
+                } else {
+                    matches.len() - (count - current_index)
+                }
             }
-        };
-        current_index
+        }
     }
 
     fn find_matches(
@@ -246,23 +246,26 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 pub fn go_to_fetched_definition(
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
 }
 
 pub fn go_to_fetched_type_definition(
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
 }
 
 fn go_to_fetched_definition_of_kind(
     kind: LinkDefinitionKind,
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
     let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
@@ -275,7 +278,7 @@ fn go_to_fetched_definition_of_kind(
             cx.focus_self();
         }
 
-        editor.navigate_to_definitions(cached_definitions, cx);
+        editor.navigate_to_definitions(cached_definitions, split, cx);
     } else {
         editor.select(
             SelectPhase::Begin {
@@ -403,7 +406,7 @@ mod tests {
             });
 
         cx.update_editor(|editor, cx| {
-            go_to_fetched_type_definition(editor, hover_point, cx);
+            go_to_fetched_type_definition(editor, hover_point, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -614,7 +617,7 @@ mod tests {
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, cx);
+            go_to_fetched_definition(editor, hover_point, false, cx);
         });
         // Assert selection moved to to definition
         cx.lsp
@@ -655,7 +658,7 @@ mod tests {
             ])))
         });
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, cx);
+            go_to_fetched_definition(editor, hover_point, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();

crates/editor/src/selections_collection.rs 🔗

@@ -16,13 +16,13 @@ use crate::{
     Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
 };
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct PendingSelection {
     pub selection: Selection<Anchor>,
     pub mode: SelectMode,
 }
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct SelectionsCollection {
     display_map: ModelHandle<DisplayMap>,
     buffer: ModelHandle<MultiBuffer>,
@@ -138,7 +138,7 @@ impl SelectionsCollection {
         .collect()
     }
 
-    // Returns all of the selections, adjusted to take into account the selection line_mode
+    /// Returns all of the selections, adjusted to take into account the selection line_mode
     pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
         let mut selections = self.all::<Point>(cx);
         if self.line_mode {

crates/feedback/src/feedback_editor.rs 🔗

@@ -60,6 +60,7 @@ pub(crate) struct FeedbackEditor {
     system_specs: SystemSpecs,
     editor: ViewHandle<Editor>,
     project: ModelHandle<Project>,
+    pub allow_submission: bool,
 }
 
 impl FeedbackEditor {
@@ -82,10 +83,15 @@ impl FeedbackEditor {
             system_specs: system_specs.clone(),
             editor,
             project,
+            allow_submission: true,
         }
     }
 
     pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
+        if !self.allow_submission {
+            return Task::ready(Ok(()));
+        }
+
         let feedback_text = self.editor.read(cx).text(cx);
         let feedback_char_count = feedback_text.chars().count();
         let feedback_text = feedback_text.trim().to_string();
@@ -122,19 +128,26 @@ impl FeedbackEditor {
             let answer = answer.recv().await;
 
             if answer == Some(0) {
+                this.update(&mut cx, |feedback_editor, cx| {
+                    feedback_editor.set_allow_submission(false, cx);
+                })
+                .log_err();
+
                 match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
                     Ok(_) => {
                         this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
                             .log_err();
                     }
+
                     Err(error) => {
                         log::error!("{}", error);
-                        this.update(&mut cx, |_, cx| {
+                        this.update(&mut cx, |feedback_editor, cx| {
                             cx.prompt(
                                 PromptLevel::Critical,
                                 FEEDBACK_SUBMISSION_ERROR_TEXT,
                                 &["OK"],
                             );
+                            feedback_editor.set_allow_submission(true, cx);
                         })
                         .log_err();
                     }
@@ -146,6 +159,11 @@ impl FeedbackEditor {
         Task::ready(Ok(()))
     }
 
+    fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext<Self>) {
+        self.allow_submission = allow_submission;
+        cx.notify();
+    }
+
     async fn submit_feedback(
         feedback_text: &str,
         zed_client: Arc<Client>,
@@ -362,8 +380,13 @@ impl Item for FeedbackEditor {
 impl SearchableItem for FeedbackEditor {
     type Match = Range<Anchor>;
 
-    fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
-        Editor::to_search_event(event)
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<workspace::searchable::SearchEvent> {
+        self.editor
+            .update(cx, |editor, cx| editor.to_search_event(event, cx))
     }
 
     fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
@@ -391,6 +414,11 @@ impl SearchableItem for FeedbackEditor {
             .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
     }
 
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |e, cx| e.select_matches(matches, cx))
+    }
+
     fn find_matches(
         &mut self,
         query: project::search::SearchQuery,

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -46,10 +46,28 @@ impl View for SubmitFeedbackButton {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let theme = theme::current(cx).clone();
+        let allow_submission = self
+            .active_item
+            .as_ref()
+            .map_or(true, |i| i.read(cx).allow_submission);
+
         enum SubmitFeedbackButton {}
         MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
-            let style = theme.feedback.submit_button.style_for(state);
-            Label::new("Submit as Markdown", style.text.clone())
+            let text;
+            let style = if allow_submission {
+                text = "Submit as Markdown";
+                theme.feedback.submit_button.style_for(state)
+            } else {
+                text = "Submitting...";
+                theme
+                    .feedback
+                    .submit_button
+                    .disabled
+                    .as_ref()
+                    .unwrap_or(&theme.feedback.submit_button.default)
+            };
+
+            Label::new(text, style.text.clone())
                 .contained()
                 .with_style(style.container)
         })

crates/file_finder/src/file_finder.rs 🔗

@@ -442,53 +442,71 @@ impl PickerDelegate for FileFinderDelegate {
         }
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<FileFinder>) {
         if let Some(m) = self.matches.get(self.selected_index()) {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let open_task = workspace.update(cx, |workspace, cx| match m {
-                    Match::History(history_match) => {
-                        let worktree_id = history_match.project.worktree_id;
-                        if workspace
-                            .project()
-                            .read(cx)
-                            .worktree_for_id(worktree_id, cx)
-                            .is_some()
-                        {
-                            workspace.open_path(
-                                ProjectPath {
-                                    worktree_id,
-                                    path: Arc::clone(&history_match.project.path),
-                                },
-                                None,
-                                true,
-                                cx,
-                            )
+                let open_task = workspace.update(cx, move |workspace, cx| {
+                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
+                        if secondary {
+                            workspace.split_path(project_path, cx)
                         } else {
-                            match history_match.absolute.as_ref() {
-                                Some(abs_path) => {
-                                    workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
-                                }
-                                None => workspace.open_path(
+                            workspace.open_path(project_path, None, true, cx)
+                        }
+                    };
+                    match m {
+                        Match::History(history_match) => {
+                            let worktree_id = history_match.project.worktree_id;
+                            if workspace
+                                .project()
+                                .read(cx)
+                                .worktree_for_id(worktree_id, cx)
+                                .is_some()
+                            {
+                                split_or_open(
+                                    workspace,
                                     ProjectPath {
                                         worktree_id,
                                         path: Arc::clone(&history_match.project.path),
                                     },
-                                    None,
-                                    true,
                                     cx,
-                                ),
+                                )
+                            } else {
+                                match history_match.absolute.as_ref() {
+                                    Some(abs_path) => {
+                                        if secondary {
+                                            workspace.split_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        } else {
+                                            workspace.open_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        }
+                                    }
+                                    None => split_or_open(
+                                        workspace,
+                                        ProjectPath {
+                                            worktree_id,
+                                            path: Arc::clone(&history_match.project.path),
+                                        },
+                                        cx,
+                                    ),
+                                }
                             }
                         }
+                        Match::Search(m) => split_or_open(
+                            workspace,
+                            ProjectPath {
+                                worktree_id: WorktreeId::from_usize(m.worktree_id),
+                                path: m.path.clone(),
+                            },
+                            cx,
+                        ),
                     }
-                    Match::Search(m) => workspace.open_path(
-                        ProjectPath {
-                            worktree_id: WorktreeId::from_usize(m.worktree_id),
-                            path: m.path.clone(),
-                        },
-                        None,
-                        true,
-                        cx,
-                    ),
                 });
 
                 let row = self

crates/fs/src/repository.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use collections::HashMap;
-use git2::{BranchType, ErrorCode};
+use git2::{BranchType, StatusShow};
 use parking_lot::Mutex;
 use rpc::proto;
 use serde_derive::{Deserialize, Serialize};
@@ -10,6 +10,7 @@ use std::{
     os::unix::prelude::OsStrExt,
     path::{Component, Path, PathBuf},
     sync::Arc,
+    time::SystemTime,
 };
 use sum_tree::{MapSeekTarget, TreeMap};
 use util::ResultExt;
@@ -25,23 +26,30 @@ pub struct Branch {
 #[async_trait::async_trait]
 pub trait GitRepository: Send {
     fn reload_index(&self);
-
     fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
-
     fn branch_name(&self) -> Option<String>;
 
-    fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
-
-    fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
-    fn branches(&self) -> Result<Vec<Branch>> {
-        Ok(vec![])
-    }
-    fn change_branch(&self, _: &str) -> Result<()> {
-        Ok(())
-    }
-    fn create_branch(&self, _: &str) -> Result<()> {
-        Ok(())
-    }
+    /// Get the statuses of all of the files in the index that start with the given
+    /// path and have changes with resepect to the HEAD commit. This is fast because
+    /// the index stores hashes of trees, so that unchanged directories can be skipped.
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the index. In the common case, when there are no changes, this only requires
+    /// an index lookup. The index stores the mtime of each file when it was added,
+    /// so there's no work to do if the mtime matches.
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the HEAD commit. In the common case, when there are no changes, this only
+    /// requires an index lookup and blob comparison between the index and the HEAD
+    /// commit. The index stores the mtime of each file when it was added, so there's
+    /// no need to consider the working directory file if the mtime matches.
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    fn branches(&self) -> Result<Vec<Branch>>;
+    fn change_branch(&self, _: &str) -> Result<()>;
+    fn create_branch(&self, _: &str) -> Result<()>;
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -50,7 +58,6 @@ impl std::fmt::Debug for dyn GitRepository {
     }
 }
 
-#[async_trait::async_trait]
 impl GitRepository for LibGitRepository {
     fn reload_index(&self) {
         if let Ok(mut index) = self.index() {
@@ -88,39 +95,67 @@ impl GitRepository for LibGitRepository {
         Some(branch.to_string())
     }
 
-    fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
-        let statuses = self.statuses(None).log_err()?;
-
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
         let mut map = TreeMap::default();
 
-        for status in statuses
-            .iter()
-            .filter(|status| !status.status().contains(git2::Status::IGNORED))
-        {
-            let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
-            let Some(status) = read_status(status.status()) else {
-                continue
-            };
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(path_prefix);
+        options.show(StatusShow::Index);
+
+        if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
+            for status in statuses.iter() {
+                let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
+                let status = status.status();
+                if !status.contains(git2::Status::IGNORED) {
+                    if let Some(status) = read_status(status) {
+                        map.insert(path, status)
+                    }
+                }
+            }
+        }
+        map
+    }
 
-            map.insert(path, status)
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        // If the file has not changed since it was added to the index, then
+        // there can't be any changes.
+        if matches_index(self, path, mtime) {
+            return None;
         }
 
-        Some(map)
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+        options.show(StatusShow::Workdir);
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
     }
 
-    fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
-        let status = self.status_file(path);
-        match status {
-            Ok(status) => Ok(read_status(status)),
-            Err(e) => {
-                if e.code() == ErrorCode::NotFound {
-                    Ok(None)
-                } else {
-                    Err(e.into())
-                }
-            }
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+
+        // If the file has not changed since it was added to the index, then
+        // there's no need to examine the working directory file: just compare
+        // the blob in the index to the one in the HEAD commit.
+        if matches_index(self, path, mtime) {
+            options.show(StatusShow::Index);
         }
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
     }
+
     fn branches(&self) -> Result<Vec<Branch>> {
         let local_branches = self.branches(Some(BranchType::Local))?;
         let valid_branches = local_branches
@@ -163,6 +198,21 @@ impl GitRepository for LibGitRepository {
     }
 }
 
+fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
+    if let Some(index) = repo.index().log_err() {
+        if let Some(entry) = index.get_path(&path, 0) {
+            if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
+                if entry.mtime.seconds() == mtime.as_secs() as i32
+                    && entry.mtime.nanoseconds() == mtime.subsec_nanos()
+                {
+                    return true;
+                }
+            }
+        }
+    }
+    false
+}
+
 fn read_status(status: git2::Status) -> Option<GitFileStatus> {
     if status.contains(git2::Status::CONFLICTED) {
         Some(GitFileStatus::Conflict)
@@ -212,18 +262,40 @@ impl GitRepository for FakeGitRepository {
         state.branch_name.clone()
     }
 
-    fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
-        let state = self.state.lock();
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
         let mut map = TreeMap::default();
+        let state = self.state.lock();
         for (repo_path, status) in state.worktree_statuses.iter() {
-            map.insert(repo_path.to_owned(), status.to_owned());
+            if repo_path.0.starts_with(path_prefix) {
+                map.insert(repo_path.to_owned(), status.to_owned());
+            }
         }
-        Some(map)
+        map
     }
 
-    fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
+    fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
+        None
+    }
+
+    fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
         let state = self.state.lock();
-        Ok(state.worktree_statuses.get(path).cloned())
+        state.worktree_statuses.get(path).cloned()
+    }
+
+    fn branches(&self) -> Result<Vec<Branch>> {
+        Ok(vec![])
+    }
+
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
+    }
+
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
     }
 }
 

crates/gpui/src/app.rs 🔗

@@ -44,6 +44,7 @@ use window_input_handler::WindowInputHandler;
 use crate::{
     elements::{AnyElement, AnyRootElement, RootElement},
     executor::{self, Task},
+    fonts::TextStyle,
     json,
     keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{
@@ -1073,7 +1074,7 @@ impl AppContext {
 
     pub fn is_action_available(&self, action: &dyn Action) -> bool {
         let mut available_in_window = false;
-        let action_type = action.as_any().type_id();
+        let action_id = action.id();
         if let Some(window_id) = self.platform.main_window_id() {
             available_in_window = self
                 .read_window(window_id, |cx| {
@@ -1083,7 +1084,7 @@ impl AppContext {
                                 cx.views_metadata.get(&(window_id, view_id))
                             {
                                 if let Some(actions) = cx.actions.get(&view_metadata.type_id) {
-                                    if actions.contains_key(&action_type) {
+                                    if actions.contains_key(&action_id) {
                                         return true;
                                     }
                                 }
@@ -1094,7 +1095,7 @@ impl AppContext {
                 })
                 .unwrap_or(false);
         }
-        available_in_window || self.global_actions.contains_key(&action_type)
+        available_in_window || self.global_actions.contains_key(&action_id)
     }
 
     fn actions_mut(
@@ -3363,6 +3364,7 @@ pub struct LayoutContext<'a, 'b, 'c, V: View> {
     view_context: &'c mut ViewContext<'a, 'b, V>,
     new_parents: &'c mut HashMap<usize, usize>,
     views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
+    text_style_stack: Vec<Arc<TextStyle>>,
     pub refreshing: bool,
 }
 
@@ -3377,6 +3379,7 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
             view_context,
             new_parents,
             views_to_notify_if_ancestors_change,
+            text_style_stack: Vec::new(),
             refreshing,
         }
     }
@@ -3399,7 +3402,7 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
         for (i, view_id) in self.ancestors(view_id).enumerate() {
             if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
                 if let Some(actions) = self.actions.get(&view_metadata.type_id) {
-                    if actions.contains_key(&action.as_any().type_id()) {
+                    if actions.contains_key(&action.id()) {
                         handler_depth = Some(i);
                     }
                 }
@@ -3407,22 +3410,18 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
             }
         }
 
-        if self.global_actions.contains_key(&action.as_any().type_id()) {
+        if self.global_actions.contains_key(&action.id()) {
             handler_depth = Some(contexts.len())
         }
 
+        let action_contexts = if let Some(depth) = handler_depth {
+            &contexts[depth..]
+        } else {
+            &contexts
+        };
+
         self.keystroke_matcher
-            .bindings_for_action_type(action.as_any().type_id())
-            .find_map(|b| {
-                let highest_handler = handler_depth?;
-                if action.eq(b.action())
-                    && (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..]))
-                {
-                    Some(b.keystrokes().into())
-                } else {
-                    None
-                }
-            })
+            .keystrokes_for_action(action, action_contexts)
     }
 
     fn notify_if_view_ancestors_change(&mut self, view_id: usize) {
@@ -3432,6 +3431,24 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
             .or_default()
             .push(self_view_id);
     }
+
+    pub fn text_style(&self) -> Arc<TextStyle> {
+        self.text_style_stack
+            .last()
+            .cloned()
+            .unwrap_or(Default::default())
+    }
+
+    pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
+    where
+        S: Into<Arc<TextStyle>>,
+        F: FnOnce(&mut Self) -> T,
+    {
+        self.text_style_stack.push(style.into());
+        let result = f(self);
+        self.text_style_stack.pop();
+        result
+    }
 }
 
 impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
@@ -3468,6 +3485,72 @@ impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
     }
 }
 
+pub struct PaintContext<'a, 'b, 'c, V: View> {
+    view_context: &'c mut ViewContext<'a, 'b, V>,
+    text_style_stack: Vec<Arc<TextStyle>>,
+}
+
+impl<'a, 'b, 'c, V: View> PaintContext<'a, 'b, 'c, V> {
+    pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
+        Self {
+            view_context,
+            text_style_stack: Vec::new(),
+        }
+    }
+
+    pub fn text_style(&self) -> Arc<TextStyle> {
+        self.text_style_stack
+            .last()
+            .cloned()
+            .unwrap_or(Default::default())
+    }
+
+    pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
+    where
+        S: Into<Arc<TextStyle>>,
+        F: FnOnce(&mut Self) -> T,
+    {
+        self.text_style_stack.push(style.into());
+        let result = f(self);
+        self.text_style_stack.pop();
+        result
+    }
+}
+
+impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
+    type Target = ViewContext<'a, 'b, V>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.view_context
+    }
+}
+
+impl<V: View> DerefMut for PaintContext<'_, '_, '_, V> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.view_context
+    }
+}
+
+impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
+    fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
+        BorrowAppContext::read_with(&*self.view_context, f)
+    }
+
+    fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
+        BorrowAppContext::update(&mut *self.view_context, f)
+    }
+}
+
+impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
+    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+        BorrowWindowContext::read_with(&*self.view_context, window_id, f)
+    }
+
+    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
+        BorrowWindowContext::update(&mut *self.view_context, window_id, f)
+    }
+}
+
 pub struct EventContext<'a, 'b, 'c, V: View> {
     view_context: &'c mut ViewContext<'a, 'b, V>,
     pub(crate) handled: bool,

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

@@ -14,7 +14,7 @@ use crate::{
     text_layout::TextLayoutCache,
     util::post_inc,
     Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
+    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, PaintContext, SceneBuilder,
     Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
@@ -363,17 +363,13 @@ impl<'a> WindowContext<'a> {
     ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
         let window_id = self.window_id;
         let mut contexts = Vec::new();
-        let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
+        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)) {
                 contexts.push(view_metadata.keymap_context.clone());
                 if let Some(actions) = self.actions.get(&view_metadata.type_id) {
-                    handler_depths_by_action_type.extend(
-                        actions
-                            .keys()
-                            .copied()
-                            .map(|action_type| (action_type, depth)),
-                    );
+                    handler_depths_by_action_id
+                        .extend(actions.keys().copied().map(|action_id| (action_id, depth)));
                 }
             } else {
                 log::error!(
@@ -383,21 +379,21 @@ impl<'a> WindowContext<'a> {
             }
         }
 
-        handler_depths_by_action_type.extend(
+        handler_depths_by_action_id.extend(
             self.global_actions
                 .keys()
                 .copied()
-                .map(|action_type| (action_type, contexts.len())),
+                .map(|action_id| (action_id, contexts.len())),
         );
 
         self.action_deserializers
             .iter()
-            .filter_map(move |(name, (type_id, deserialize))| {
-                if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
+            .filter_map(move |(name, (action_id, deserialize))| {
+                if let Some(action_depth) = handler_depths_by_action_id.get(action_id).copied() {
                     let action = deserialize(serde_json::Value::Object(Default::default())).ok()?;
                     let bindings = self
                         .keystroke_matcher
-                        .bindings_for_action_type(*type_id)
+                        .bindings_for_action(*action_id)
                         .filter(|b| {
                             action.eq(b.action())
                                 && (0..=action_depth)
@@ -434,11 +430,7 @@ impl<'a> WindowContext<'a> {
                 MatchResult::None => false,
                 MatchResult::Pending => true,
                 MatchResult::Matches(matches) => {
-                    let no_action_id = (NoAction {}).id();
                     for (view_id, action) in matches {
-                        if action.id() == no_action_id {
-                            return false;
-                        }
                         if self.dispatch_action(Some(*view_id), action.as_ref()) {
                             self.keystroke_matcher.clear_pending();
                             handled_by = Some(action.boxed_clone());
@@ -1408,7 +1400,7 @@ impl<V: View> Element<V> for ChildView {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
             rendered_view

crates/gpui/src/elements.rs 🔗

@@ -34,8 +34,8 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle,
-    WindowContext,
+    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    WeakViewHandle, WindowContext,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
@@ -62,7 +62,7 @@ pub trait Element<V: View>: 'static {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState;
 
     fn rect_for_text_range(
@@ -299,7 +299,14 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 mut layout,
             } => {
                 let bounds = RectF::new(origin, size);
-                let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx);
+                let paint = element.paint(
+                    scene,
+                    bounds,
+                    visible_bounds,
+                    &mut layout,
+                    view,
+                    &mut PaintContext::new(cx),
+                );
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -317,7 +324,14 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 ..
             } => {
                 let bounds = RectF::new(origin, bounds.size());
-                let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx);
+                let paint = element.paint(
+                    scene,
+                    bounds,
+                    visible_bounds,
+                    &mut layout,
+                    view,
+                    &mut PaintContext::new(cx),
+                );
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -514,7 +528,7 @@ impl<V: View> Element<V> for AnyElement<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.paint(scene, bounds.origin(), visible_bounds, view, cx);
     }

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

@@ -1,6 +1,7 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use json::ToJson;
 
@@ -69,7 +70,7 @@ impl<V: View> Element<V> for Align<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let my_center = bounds.size() / 2.;
         let my_target = my_center + my_center * self.alignment;

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

@@ -3,7 +3,7 @@ use std::marker::PhantomData;
 use super::Element;
 use crate::{
     json::{self, json},
-    SceneBuilder, View, ViewContext,
+    PaintContext, SceneBuilder, View, ViewContext,
 };
 use json::ToJson;
 use pathfinder_geometry::{
@@ -56,7 +56,7 @@ where
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.0(scene, bounds, visible_bounds, view, cx)
     }

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

@@ -4,7 +4,8 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 use serde_json::json;
 
 use crate::{
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct Clipped<V: View> {
@@ -37,7 +38,7 @@ impl<V: View> Element<V> for Clipped<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.paint_layer(Some(bounds), |scene| {
             self.child

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

@@ -5,7 +5,8 @@ use serde_json::json;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct ConstrainedBox<V: View> {
@@ -156,7 +157,7 @@ impl<V: View> Element<V> for ConstrainedBox<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.paint_layer(Some(visible_bounds), |scene| {
             self.child

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

@@ -10,7 +10,8 @@ use crate::{
     json::ToJson,
     platform::CursorStyle,
     scene::{self, Border, CursorRegion, Quad},
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -214,7 +215,7 @@ impl<V: View> Element<V> for Container<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let quad_bounds = RectF::from_points(
             bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),

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

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    LayoutContext, SceneBuilder, View, ViewContext,
+    LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
 };
 use crate::{Element, SizeConstraint};
 
@@ -57,7 +57,7 @@ impl<V: View> Element<V> for Empty {
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) -> Self::PaintState {
     }
 

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

@@ -2,7 +2,8 @@ use std::ops::Range;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use serde_json::json;
 
@@ -61,7 +62,7 @@ impl<V: View> Element<V> for Expanded<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

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

@@ -2,8 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
-    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint,
-    Vector2FExt, View, ViewContext,
+    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
+    SizeConstraint, Vector2FExt, View, ViewContext,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -258,7 +258,7 @@ impl<V: View> Element<V> for Flex<V> {
         visible_bounds: RectF,
         remaining_space: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -449,7 +449,7 @@ impl<V: View> Element<V> for FlexItem<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx)

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

@@ -3,7 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct Hook<V: View> {
@@ -52,7 +53,7 @@ impl<V: View> Element<V> for Hook<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

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

@@ -5,8 +5,8 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -97,7 +97,7 @@ impl<V: View> Element<V> for Image {
         _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) -> Self::PaintState {
         if let Some(data) = layout {
             scene.push_image(scene::Image {

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

@@ -66,7 +66,7 @@ impl<V: View> Element<V> for KeystrokeLabel {
         visible_bounds: RectF,
         element: &mut AnyElement<V>,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         element.paint(scene, bounds.origin(), visible_bounds, view, cx);
     }

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

@@ -8,7 +8,7 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle},
-    Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -163,7 +163,7 @@ impl<V: View> Element<V> for Label {
         visible_bounds: RectF,
         line: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         line.paint(

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

@@ -4,8 +4,8 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::json,
-    AnyElement, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
+    View, ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
@@ -255,7 +255,7 @@ impl<V: View> Element<V> for List<V> {
         visible_bounds: RectF,
         scroll_top: &mut ListOffset,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
         scene.push_layer(Some(visible_bounds));
@@ -647,7 +647,7 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::Empty, geometry::vector::vec2f, Entity};
+    use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext};
     use rand::prelude::*;
     use std::env;
 
@@ -988,7 +988,7 @@ mod tests {
             _: RectF,
             _: &mut (),
             _: &mut V,
-            _: &mut ViewContext<V>,
+            _: &mut PaintContext<V>,
         ) {
             unimplemented!()
         }

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

@@ -10,8 +10,8 @@ use crate::{
         CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
         MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
-    AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
-    SizeConstraint, View, ViewContext,
+    AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext,
+    SceneBuilder, SizeConstraint, View, ViewContext,
 };
 use serde_json::json;
 use std::{marker::PhantomData, ops::Range};
@@ -256,7 +256,7 @@ impl<Tag, V: View> Element<V> for MouseEventHandler<Tag, V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         if self.above {
             self.child

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

@@ -3,8 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
+    SizeConstraint, View, ViewContext,
 };
 use serde_json::json;
 
@@ -143,7 +143,7 @@ impl<V: View> Element<V> for Overlay<V> {
         _: RectF,
         size: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         let (anchor_position, mut bounds) = match self.position_mode {
             OverlayPositionMode::Window => {

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

@@ -7,8 +7,8 @@ use crate::{
     geometry::rect::RectF,
     platform::{CursorStyle, MouseButton},
     scene::MouseDrag,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
+    SizeConstraint, View, ViewContext,
 };
 
 #[derive(Copy, Clone, Debug)]
@@ -125,7 +125,7 @@ impl<V: View> Element<V> for Resizable<V> {
         visible_bounds: pathfinder_geometry::rect::RectF,
         constraint: &mut SizeConstraint,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.push_stacking_context(None, None);
 

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

@@ -3,7 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{self, json, ToJson},
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 /// Element which renders it's children in a stack on top of each other.
@@ -57,7 +58,7 @@ impl<V: View> Element<V> for Stack<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         for child in &mut self.children {
             scene.paint_layer(None, |scene| {

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

@@ -1,5 +1,6 @@
 use super::constrain_size_preserving_aspect_ratio;
 use crate::json::ToJson;
+use crate::PaintContext;
 use crate::{
     color::Color,
     geometry::{
@@ -73,7 +74,7 @@ impl<V: View> Element<V> for Svg {
         _visible_bounds: RectF,
         svg: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) {
         if let Some(svg) = svg.clone() {
             scene.push_icon(scene::Icon {

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

@@ -7,8 +7,8 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle, ShapedBoundary},
-    AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
-    View, ViewContext,
+    AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    TextLayoutCache, View, ViewContext,
 };
 use log::warn;
 use serde_json::json;
@@ -171,7 +171,7 @@ impl<V: View> Element<V> for Text {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let mut origin = bounds.origin();
         let empty = Vec::new();

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

@@ -6,8 +6,8 @@ use crate::{
     fonts::TextStyle,
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
-    ViewContext,
+    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    Task, View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -194,7 +194,7 @@ impl<V: View> Element<V> for Tooltip<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

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

@@ -6,7 +6,7 @@ use crate::{
     },
     json::{self, json},
     platform::ScrollWheelEvent,
-    AnyElement, LayoutContext, MouseRegion, SceneBuilder, View, ViewContext,
+    AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, View, ViewContext,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -278,7 +278,7 @@ impl<V: View> Element<V> for UniformList<V> {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
 

crates/gpui/src/fonts.rs 🔗

@@ -71,6 +71,32 @@ pub struct TextStyle {
     pub underline: Underline,
 }
 
+impl TextStyle {
+    pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle {
+        TextStyle {
+            color: refinement.color.unwrap_or(self.color),
+            font_family_name: refinement
+                .font_family_name
+                .unwrap_or_else(|| self.font_family_name.clone()),
+            font_family_id: refinement.font_family_id.unwrap_or(self.font_family_id),
+            font_id: refinement.font_id.unwrap_or(self.font_id),
+            font_size: refinement.font_size.unwrap_or(self.font_size),
+            font_properties: refinement.font_properties.unwrap_or(self.font_properties),
+            underline: refinement.underline.unwrap_or(self.underline),
+        }
+    }
+}
+
+pub struct TextStyleRefinement {
+    pub color: Option<Color>,
+    pub font_family_name: Option<Arc<str>>,
+    pub font_family_id: Option<FamilyId>,
+    pub font_id: Option<FontId>,
+    pub font_size: Option<f32>,
+    pub font_properties: Option<Properties>,
+    pub underline: Option<Underline>,
+}
+
 #[derive(JsonSchema)]
 #[serde(remote = "Properties")]
 pub struct PropertiesDef {

crates/gpui/src/keymap_matcher.rs 🔗

@@ -8,7 +8,7 @@ use std::{any::TypeId, fmt::Debug};
 use collections::HashMap;
 use smallvec::SmallVec;
 
-use crate::Action;
+use crate::{Action, NoAction};
 
 pub use binding::{Binding, BindingMatchResult};
 pub use keymap::Keymap;
@@ -47,8 +47,8 @@ impl KeymapMatcher {
         self.keymap.clear();
     }
 
-    pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
-        self.keymap.bindings_for_action_type(action_type)
+    pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
+        self.keymap.bindings_for_action(action_id)
     }
 
     pub fn clear_pending(&mut self) {
@@ -81,6 +81,7 @@ impl KeymapMatcher {
         // The key is the reverse position of the binding in the bindings list so that later bindings
         // match before earlier ones in the user's config
         let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
+        let no_action_id = (NoAction {}).id();
 
         let first_keystroke = self.pending_keystrokes.is_empty();
         self.pending_keystrokes.push(keystroke.clone());
@@ -108,7 +109,9 @@ impl KeymapMatcher {
                 match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
                 {
                     BindingMatchResult::Complete(action) => {
-                        matched_bindings.push((*view_id, action));
+                        if action.id() != no_action_id {
+                            matched_bindings.push((*view_id, action));
+                        }
                     }
                     BindingMatchResult::Partial => {
                         self.pending_views

crates/gpui/src/keymap_matcher/binding.rs 🔗

@@ -7,8 +7,8 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke};
 
 pub struct Binding {
     action: Box<dyn Action>,
-    keystrokes: SmallVec<[Keystroke; 2]>,
-    context_predicate: Option<KeymapContextPredicate>,
+    pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
+    pub(super) context_predicate: Option<KeymapContextPredicate>,
 }
 
 impl std::fmt::Debug for Binding {

crates/gpui/src/keymap_matcher/keymap.rs 🔗

@@ -1,61 +1,388 @@
+use collections::HashSet;
 use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    collections::HashMap,
-};
+use std::{any::TypeId, collections::HashMap};
 
-use super::Binding;
+use crate::{Action, NoAction};
+
+use super::{Binding, KeymapContextPredicate, Keystroke};
 
 #[derive(Default)]
 pub struct Keymap {
     bindings: Vec<Binding>,
-    binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
+    binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
+    disabled_keystrokes: HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeymapContextPredicate>>>,
 }
 
 impl Keymap {
-    pub fn new(bindings: Vec<Binding>) -> Self {
-        let mut binding_indices_by_action_type = HashMap::new();
-        for (ix, binding) in bindings.iter().enumerate() {
-            binding_indices_by_action_type
-                .entry(binding.action().type_id())
-                .or_insert_with(SmallVec::new)
-                .push(ix);
-        }
-
-        Self {
-            binding_indices_by_action_type,
-            bindings,
-        }
+    #[cfg(test)]
+    pub(super) fn new(bindings: Vec<Binding>) -> Self {
+        let mut this = Self::default();
+        this.add_bindings(bindings);
+        this
     }
 
-    pub(crate) fn bindings_for_action_type(
+    pub(crate) fn bindings_for_action(
         &self,
-        action_type: TypeId,
+        action_id: TypeId,
     ) -> impl Iterator<Item = &'_ Binding> {
-        self.binding_indices_by_action_type
-            .get(&action_type)
+        self.binding_indices_by_action_id
+            .get(&action_id)
             .map(SmallVec::as_slice)
             .unwrap_or(&[])
             .iter()
             .map(|ix| &self.bindings[*ix])
+            .filter(|binding| !self.binding_disabled(binding))
     }
 
     pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
+        let no_action_id = (NoAction {}).id();
+        let mut new_bindings = Vec::new();
+        let mut has_new_disabled_keystrokes = false;
         for binding in bindings {
-            self.binding_indices_by_action_type
-                .entry(binding.action().as_any().type_id())
-                .or_default()
-                .push(self.bindings.len());
-            self.bindings.push(binding);
+            if binding.action().id() == no_action_id {
+                has_new_disabled_keystrokes |= self
+                    .disabled_keystrokes
+                    .entry(binding.keystrokes)
+                    .or_default()
+                    .insert(binding.context_predicate);
+            } else {
+                new_bindings.push(binding);
+            }
+        }
+
+        if has_new_disabled_keystrokes {
+            self.binding_indices_by_action_id.retain(|_, indices| {
+                indices.retain(|ix| {
+                    let binding = &self.bindings[*ix];
+                    match self.disabled_keystrokes.get(&binding.keystrokes) {
+                        Some(disabled_predicates) => {
+                            !disabled_predicates.contains(&binding.context_predicate)
+                        }
+                        None => true,
+                    }
+                });
+                !indices.is_empty()
+            });
+        }
+
+        for new_binding in new_bindings {
+            if !self.binding_disabled(&new_binding) {
+                self.binding_indices_by_action_id
+                    .entry(new_binding.action().id())
+                    .or_default()
+                    .push(self.bindings.len());
+                self.bindings.push(new_binding);
+            }
         }
     }
 
     pub(crate) fn clear(&mut self) {
         self.bindings.clear();
-        self.binding_indices_by_action_type.clear();
+        self.binding_indices_by_action_id.clear();
+        self.disabled_keystrokes.clear();
+    }
+
+    pub fn bindings(&self) -> Vec<&Binding> {
+        self.bindings
+            .iter()
+            .filter(|binding| !self.binding_disabled(binding))
+            .collect()
+    }
+
+    fn binding_disabled(&self, binding: &Binding) -> bool {
+        match self.disabled_keystrokes.get(&binding.keystrokes) {
+            Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
+            None => false,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::actions;
+
+    use super::*;
+
+    actions!(
+        keymap_test,
+        [Present1, Present2, Present3, Duplicate, Missing]
+    );
+
+    #[test]
+    fn regular_keymap() {
+        let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+        let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+        let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+        let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+        let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+        let missing = Binding::new("ctrl-r", Missing {}, None);
+        let all_bindings = [
+            &present_1,
+            &present_2,
+            &present_3,
+            &keystroke_duplicate_to_1,
+            &full_duplicate_to_2,
+            &missing,
+        ];
+
+        let mut keymap = Keymap::default();
+        assert_absent(&keymap, &all_bindings);
+        assert!(keymap.bindings().is_empty());
+
+        keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
+        assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
+        assert_present(
+            &keymap,
+            &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
+        );
+
+        keymap.add_bindings([
+            keystroke_duplicate_to_1.clone(),
+            full_duplicate_to_2.clone(),
+        ]);
+        assert_absent(&keymap, &[&missing]);
+        assert!(
+            !keymap.binding_disabled(&keystroke_duplicate_to_1),
+            "Duplicate binding 1 was added and should not be disabled"
+        );
+        assert!(
+            !keymap.binding_disabled(&full_duplicate_to_2),
+            "Duplicate binding 2 was added and should not be disabled"
+        );
+
+        assert_eq!(
+            keymap
+                .bindings_for_action(keystroke_duplicate_to_1.action().id())
+                .map(|binding| &binding.keystrokes)
+                .flatten()
+                .collect::<Vec<_>>(),
+            vec![&Keystroke {
+                ctrl: true,
+                alt: false,
+                shift: false,
+                cmd: false,
+                function: false,
+                key: "q".to_string()
+            }],
+            "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
+        );
+        assert_eq!(
+            keymap
+                .bindings_for_action(full_duplicate_to_2.action().id())
+                .map(|binding| &binding.keystrokes)
+                .flatten()
+                .collect::<Vec<_>>(),
+            vec![
+                &Keystroke {
+                    ctrl: true,
+                    alt: false,
+                    shift: false,
+                    cmd: false,
+                    function: false,
+                    key: "w".to_string()
+                },
+                &Keystroke {
+                    ctrl: true,
+                    alt: false,
+                    shift: false,
+                    cmd: false,
+                    function: false,
+                    key: "w".to_string()
+                }
+            ],
+            "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
+        );
+
+        let updated_bindings = keymap.bindings();
+        let expected_updated_bindings = vec![
+            &present_1,
+            &present_2,
+            &present_3,
+            &keystroke_duplicate_to_1,
+            &full_duplicate_to_2,
+        ];
+        assert_eq!(
+            updated_bindings.len(),
+            expected_updated_bindings.len(),
+            "Unexpected updated keymap bindings {updated_bindings:?}"
+        );
+        for (i, expected) in expected_updated_bindings.iter().enumerate() {
+            let keymap_binding = &updated_bindings[i];
+            assert_eq!(
+                keymap_binding.context_predicate, expected.context_predicate,
+                "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+            );
+            assert_eq!(
+                keymap_binding.keystrokes, expected.keystrokes,
+                "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+            );
+        }
+
+        keymap.clear();
+        assert_absent(&keymap, &all_bindings);
+        assert!(keymap.bindings().is_empty());
     }
 
-    pub fn bindings(&self) -> &Vec<Binding> {
-        &self.bindings
+    #[test]
+    fn keymap_with_ignored() {
+        let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+        let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+        let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+        let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+        let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+        let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
+        let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
+        let ignored_3_with_other_context =
+            Binding::new("ctrl-e", NoAction {}, Some("other_context"));
+
+        let mut keymap = Keymap::default();
+
+        keymap.add_bindings([
+            ignored_1.clone(),
+            ignored_2.clone(),
+            ignored_3_with_other_context.clone(),
+        ]);
+        assert_absent(&keymap, &[&present_3]);
+        assert_disabled(
+            &keymap,
+            &[
+                &present_1,
+                &present_2,
+                &ignored_1,
+                &ignored_2,
+                &ignored_3_with_other_context,
+            ],
+        );
+        assert!(keymap.bindings().is_empty());
+        keymap.clear();
+
+        keymap.add_bindings([
+            present_1.clone(),
+            present_2.clone(),
+            present_3.clone(),
+            ignored_1.clone(),
+            ignored_2.clone(),
+            ignored_3_with_other_context.clone(),
+        ]);
+        assert_present(&keymap, &[(&present_3, "e")]);
+        assert_disabled(
+            &keymap,
+            &[
+                &present_1,
+                &present_2,
+                &ignored_1,
+                &ignored_2,
+                &ignored_3_with_other_context,
+            ],
+        );
+        keymap.clear();
+
+        keymap.add_bindings([
+            present_1.clone(),
+            present_2.clone(),
+            present_3.clone(),
+            ignored_1.clone(),
+        ]);
+        assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
+        assert_disabled(&keymap, &[&present_1, &ignored_1]);
+        assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
+        keymap.clear();
+
+        keymap.add_bindings([
+            present_1.clone(),
+            present_2.clone(),
+            present_3.clone(),
+            keystroke_duplicate_to_1.clone(),
+            full_duplicate_to_2.clone(),
+            ignored_1.clone(),
+            ignored_2.clone(),
+            ignored_3_with_other_context.clone(),
+        ]);
+        assert_present(&keymap, &[(&present_3, "e")]);
+        assert_disabled(
+            &keymap,
+            &[
+                &present_1,
+                &present_2,
+                &keystroke_duplicate_to_1,
+                &full_duplicate_to_2,
+                &ignored_1,
+                &ignored_2,
+                &ignored_3_with_other_context,
+            ],
+        );
+        keymap.clear();
+    }
+
+    #[track_caller]
+    fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
+        let keymap_bindings = keymap.bindings();
+        assert_eq!(
+            expected_bindings.len(),
+            keymap_bindings.len(),
+            "Unexpected keymap bindings {keymap_bindings:?}"
+        );
+        for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
+            assert!(
+                !keymap.binding_disabled(expected),
+                "{expected:?} should not be disabled as it was added into keymap for element {i}"
+            );
+            assert_eq!(
+                keymap
+                    .bindings_for_action(expected.action().id())
+                    .map(|binding| &binding.keystrokes)
+                    .flatten()
+                    .collect::<Vec<_>>(),
+                vec![&Keystroke {
+                    ctrl: true,
+                    alt: false,
+                    shift: false,
+                    cmd: false,
+                    function: false,
+                    key: expected_key.to_string()
+                }],
+                "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
+            );
+
+            let keymap_binding = &keymap_bindings[i];
+            assert_eq!(
+                keymap_binding.context_predicate, expected.context_predicate,
+                "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+            );
+            assert_eq!(
+                keymap_binding.keystrokes, expected.keystrokes,
+                "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+            );
+        }
+    }
+
+    #[track_caller]
+    fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
+        for binding in bindings.iter() {
+            assert!(
+                !keymap.binding_disabled(binding),
+                "{binding:?} should not be disabled in the keymap where was not added"
+            );
+            assert_eq!(
+                keymap.bindings_for_action(binding.action().id()).count(),
+                0,
+                "{binding:?} should have no actions in the keymap where was not added"
+            );
+        }
+    }
+
+    #[track_caller]
+    fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
+        for binding in bindings.iter() {
+            assert!(
+                keymap.binding_disabled(binding),
+                "{binding:?} should be disabled in the keymap"
+            );
+            assert_eq!(
+                keymap.bindings_for_action(binding.action().id()).count(),
+                0,
+                "{binding:?} should have no actions in the keymap where it was disabled"
+            );
+        }
     }
 }

crates/gpui/src/keymap_matcher/keymap_context.rs 🔗

@@ -44,7 +44,7 @@ impl KeymapContext {
     }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 pub enum KeymapContextPredicate {
     Identifier(String),
     Equal(String, String),

crates/gpui/src/keymap_matcher/keystroke.rs 🔗

@@ -3,7 +3,7 @@ use std::fmt::Write;
 use anyhow::anyhow;
 use serde::Deserialize;
 
-#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)]
+#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 pub struct Keystroke {
     pub ctrl: bool,
     pub alt: bool,

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

@@ -231,7 +231,7 @@ impl MacForegroundPlatform {
             } => {
                 // TODO
                 let keystrokes = keystroke_matcher
-                    .bindings_for_action_type(action.as_any().type_id())
+                    .bindings_for_action(action.id())
                     .find(|binding| binding.action().eq(action.as_ref()))
                     .map(|binding| binding.keystrokes());
                 let selector = match os_action {

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

@@ -10,8 +10,8 @@ use crate::{
         mac::{
             platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer, screen::Screen,
         },
-        Event, InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
-        MouseMovedEvent, Scene, WindowBounds, WindowKind,
+        Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton,
+        MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind,
     },
 };
 use block::ConcreteBlock;
@@ -1053,7 +1053,44 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
 
     let window_height = window_state_borrow.content_size().y();
     let event = unsafe { Event::from_native(native_event, Some(window_height)) };
-    if let Some(event) = event {
+
+    if let Some(mut event) = event {
+        let synthesized_second_event = match &mut event {
+            Event::MouseDown(
+                event @ MouseButtonEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { ctrl: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseButtonEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        ctrl: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
+
+                Some(Event::MouseUp(MouseButtonEvent {
+                    button: MouseButton::Right,
+                    ..*event
+                }))
+            }
+
+            // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
+            // the ctrl-left_up to avoid having a mismatch in button down/up events if the
+            // user is still holding ctrl when releasing the left mouse button
+            Event::MouseUp(MouseButtonEvent {
+                button: MouseButton::Left,
+                modifiers: Modifiers { ctrl: true, .. },
+                ..
+            }) => return,
+
+            _ => None,
+        };
+
         match &event {
             Event::MouseMoved(
                 event @ MouseMovedEvent {
@@ -1105,6 +1142,9 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         if let Some(mut callback) = window_state_borrow.event_callback.take() {
             drop(window_state_borrow);
             callback(event);
+            if let Some(event) = synthesized_second_event {
+                callback(event);
+            }
             window_state.borrow_mut().event_callback = Some(callback);
         }
     }

crates/language/Cargo.toml 🔗

@@ -46,7 +46,6 @@ lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-rand = { workspace = true, optional = true }
 regex.workspace = true
 schemars.workspace = true
 serde.workspace = true
@@ -56,10 +55,12 @@ similar = "1.3"
 smallvec.workspace = true
 smol.workspace = true
 tree-sitter.workspace = true
-tree-sitter-rust = { version = "*", optional = true }
-tree-sitter-typescript = { version = "*", optional = true }
 unicase = "2.6"
 
+rand = { workspace = true, optional = true }
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
@@ -74,12 +75,13 @@ indoc.workspace = true
 rand.workspace = true
 unindent.workspace = true
 
-tree-sitter-embedded-template = "*"
-tree-sitter-html = "*"
-tree-sitter-javascript = "*"
-tree-sitter-json = "*"
-tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
-tree-sitter-rust = "*"
-tree-sitter-python = "*"
-tree-sitter-typescript = "*"
-tree-sitter-ruby = "*"
+tree-sitter-embedded-template.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-markdown.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-python.workspace = true
+tree-sitter-typescript.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-heex.workspace = true

crates/language/src/buffer.rs 🔗

@@ -2145,23 +2145,27 @@ impl BufferSnapshot {
 
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
         let offset = position.to_offset(self);
+        let mut range = 0..self.len();
+        let mut scope = self.language.clone().map(|language| LanguageScope {
+            language,
+            override_id: None,
+        });
 
-        if let Some(layer_info) = self
-            .syntax
-            .layers_for_range(offset..offset, &self.text)
-            .filter(|l| l.node().end_byte() > offset)
-            .last()
-        {
-            Some(LanguageScope {
-                language: layer_info.language.clone(),
-                override_id: layer_info.override_id(offset, &self.text),
-            })
-        } else {
-            self.language.clone().map(|language| LanguageScope {
-                language,
-                override_id: None,
-            })
+        // Use the layer that has the smallest node intersecting the given point.
+        for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
+            let mut cursor = layer.node().walk();
+            while cursor.goto_first_child_for_byte(offset).is_some() {}
+            let node_range = cursor.node().byte_range();
+            if node_range.to_inclusive().contains(&offset) && node_range.len() < range.len() {
+                range = node_range;
+                scope = Some(LanguageScope {
+                    language: layer.language.clone(),
+                    override_id: layer.override_id(offset, &self.text),
+                });
+            }
         }
+
+        scope
     }
 
     pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {

crates/language/src/buffer_tests.rs 🔗

@@ -1533,47 +1533,9 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
         ])
     });
 
-    let html_language = Arc::new(
-        Language::new(
-            LanguageConfig {
-                name: "HTML".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_html::language()),
-        )
-        .with_indents_query(
-            "
-            (element
-              (start_tag) @start
-              (end_tag)? @end) @indent
-            ",
-        )
-        .unwrap()
-        .with_injection_query(
-            r#"
-            (script_element
-                (raw_text) @content
-                (#set! "language" "javascript"))
-            "#,
-        )
-        .unwrap(),
-    );
+    let html_language = Arc::new(html_lang());
 
-    let javascript_language = Arc::new(
-        Language::new(
-            LanguageConfig {
-                name: "JavaScript".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_javascript::language()),
-        )
-        .with_indents_query(
-            r#"
-            (object "}" @end) @indent
-            "#,
-        )
-        .unwrap(),
-    );
+    let javascript_language = Arc::new(javascript_lang());
 
     let language_registry = Arc::new(LanguageRegistry::test());
     language_registry.add(html_language.clone());
@@ -1669,7 +1631,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
 }
 
 #[gpui::test]
-fn test_language_config_at(cx: &mut AppContext) {
+fn test_language_scope_at(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
@@ -1709,7 +1671,7 @@ fn test_language_config_at(cx: &mut AppContext) {
                 .collect(),
                 ..Default::default()
             },
-            Some(tree_sitter_javascript::language()),
+            Some(tree_sitter_typescript::language_tsx()),
         )
         .with_override_query(
             r#"
@@ -1756,6 +1718,54 @@ fn test_language_config_at(cx: &mut AppContext) {
     });
 }
 
+#[gpui::test]
+fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.add_model(|cx| {
+        let text = r#"
+            <ol>
+            <% people.each do |person| %>
+                <li>
+                    <%= person.name %>
+                </li>
+            <% end %>
+            </ol>
+        "#
+        .unindent();
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(ruby_lang()));
+        language_registry.add(Arc::new(html_lang()));
+        language_registry.add(Arc::new(erb_lang()));
+
+        let mut buffer = Buffer::new(0, text, cx);
+        buffer.set_language_registry(language_registry.clone());
+        buffer.set_language(
+            language_registry
+                .language_for_name("ERB")
+                .now_or_never()
+                .unwrap()
+                .ok(),
+            cx,
+        );
+
+        let snapshot = buffer.snapshot();
+        let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
+        assert_eq!(html_config.line_comment_prefix(), None);
+        assert_eq!(
+            html_config.block_comment_delimiters(),
+            Some((&"<!--".into(), &"-->".into()))
+        );
+
+        let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
+        assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
+        assert_eq!(ruby_config.block_comment_delimiters(), None);
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_serialization(cx: &mut gpui::AppContext) {
     let mut now = Instant::now();
@@ -2143,6 +2153,7 @@ fn ruby_lang() -> Language {
         LanguageConfig {
             name: "Ruby".into(),
             path_suffixes: vec!["rb".to_string()],
+            line_comment: Some("# ".into()),
             ..Default::default()
         },
         Some(tree_sitter_ruby::language()),
@@ -2158,6 +2169,61 @@ fn ruby_lang() -> Language {
     .unwrap()
 }
 
+fn html_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HTML".into(),
+            block_comment: Some(("<!--".into(), "-->".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_html::language()),
+    )
+    .with_indents_query(
+        "
+        (element
+          (start_tag) @start
+          (end_tag)? @end) @indent
+        ",
+    )
+    .unwrap()
+    .with_injection_query(
+        r#"
+        (script_element
+            (raw_text) @content
+            (#set! "language" "javascript"))
+        "#,
+    )
+    .unwrap()
+}
+
+fn erb_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "ERB".into(),
+            path_suffixes: vec!["erb".to_string()],
+            block_comment: Some(("<%#".into(), "%>".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_embedded_template::language()),
+    )
+    .with_injection_query(
+        r#"
+            (
+                (code) @content
+                (#set! "language" "ruby")
+                (#set! "combined")
+            )
+
+            (
+                (content) @content
+                (#set! "language" "html")
+                (#set! "combined")
+            )
+        "#,
+    )
+    .unwrap()
+}
+
 fn rust_lang() -> Language {
     Language::new(
         LanguageConfig {
@@ -2227,7 +2293,7 @@ fn javascript_lang() -> Language {
             name: "JavaScript".into(),
             ..Default::default()
         },
-        Some(tree_sitter_javascript::language()),
+        Some(tree_sitter_typescript::language_tsx()),
     )
     .with_brackets_query(
         r#"
@@ -2236,6 +2302,12 @@ fn javascript_lang() -> Language {
         "#,
     )
     .unwrap()
+    .with_indents_query(
+        r#"
+        (object "}" @end) @indent
+        "#,
+    )
+    .unwrap()
 }
 
 fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {

crates/language/src/language.rs 🔗

@@ -831,6 +831,7 @@ impl LanguageRegistry {
                                     Ok(language) => {
                                         let language = Arc::new(language);
                                         let mut state = this.state.write();
+
                                         state.add(language.clone());
                                         state.mark_language_loaded(id);
                                         if let Some(mut txs) = state.loading_languages.remove(&id) {
@@ -1791,7 +1792,7 @@ mod tests {
                 first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
                 ..Default::default()
             },
-            tree_sitter_javascript::language(),
+            tree_sitter_typescript::language_tsx(),
             vec![],
             |_| Default::default(),
         );

crates/language/src/language_settings.rs 🔗

@@ -44,6 +44,8 @@ pub struct LanguageSettings {
     pub hard_tabs: bool,
     pub soft_wrap: SoftWrap,
     pub preferred_line_length: u32,
+    pub show_wrap_guides: bool,
+    pub wrap_guides: Vec<usize>,
     pub format_on_save: FormatOnSave,
     pub remove_trailing_whitespace_on_save: bool,
     pub ensure_final_newline_on_save: bool,
@@ -84,6 +86,10 @@ pub struct LanguageSettingsContent {
     #[serde(default)]
     pub preferred_line_length: Option<u32>,
     #[serde(default)]
+    pub show_wrap_guides: Option<bool>,
+    #[serde(default)]
+    pub wrap_guides: Option<Vec<usize>>,
+    #[serde(default)]
     pub format_on_save: Option<FormatOnSave>,
     #[serde(default)]
     pub remove_trailing_whitespace_on_save: Option<bool>,
@@ -378,6 +384,9 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
     merge(&mut settings.tab_size, src.tab_size);
     merge(&mut settings.hard_tabs, src.hard_tabs);
     merge(&mut settings.soft_wrap, src.soft_wrap);
+    merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
+    merge(&mut settings.wrap_guides, src.wrap_guides.clone());
+
     merge(
         &mut settings.preferred_line_length,
         src.preferred_line_length,

crates/language/src/syntax_map.rs 🔗

@@ -569,11 +569,19 @@ impl SyntaxSnapshot {
                                 range.end = range.end.saturating_sub(step_start_byte);
                             }
 
-                            included_ranges = splice_included_ranges(
+                            let changed_indices;
+                            (included_ranges, changed_indices) = splice_included_ranges(
                                 old_tree.included_ranges(),
                                 &parent_layer_changed_ranges,
                                 &included_ranges,
                             );
+                            insert_newlines_between_ranges(
+                                changed_indices,
+                                &mut included_ranges,
+                                &text,
+                                step_start_byte,
+                                step_start_point,
+                            );
                         }
 
                         if included_ranges.is_empty() {
@@ -586,7 +594,7 @@ impl SyntaxSnapshot {
                         }
 
                         log::trace!(
-                            "update layer. language:{}, start:{:?}, ranges:{:?}",
+                            "update layer. language:{}, start:{:?}, included_ranges:{:?}",
                             language.name(),
                             LogAnchorRange(&step.range, text),
                             LogIncludedRanges(&included_ranges),
@@ -608,6 +616,16 @@ impl SyntaxSnapshot {
                             }),
                         );
                     } else {
+                        if matches!(step.mode, ParseMode::Combined { .. }) {
+                            insert_newlines_between_ranges(
+                                0..included_ranges.len(),
+                                &mut included_ranges,
+                                text,
+                                step_start_byte,
+                                step_start_point,
+                            );
+                        }
+
                         if included_ranges.is_empty() {
                             included_ranges.push(tree_sitter::Range {
                                 start_byte: 0,
@@ -771,8 +789,10 @@ impl SyntaxSnapshot {
         range: Range<T>,
         buffer: &'a BufferSnapshot,
     ) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
-        let start = buffer.anchor_before(range.start.to_offset(buffer));
-        let end = buffer.anchor_after(range.end.to_offset(buffer));
+        let start_offset = range.start.to_offset(buffer);
+        let end_offset = range.end.to_offset(buffer);
+        let start = buffer.anchor_before(start_offset);
+        let end = buffer.anchor_after(end_offset);
 
         let mut cursor = self.layers.filter::<_, ()>(move |summary| {
             if summary.max_depth > summary.min_depth {
@@ -787,20 +807,21 @@ impl SyntaxSnapshot {
         cursor.next(buffer);
         iter::from_fn(move || {
             while let Some(layer) = cursor.item() {
+                let mut info = None;
                 if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
-                    let info = SyntaxLayerInfo {
+                    let layer_start_offset = layer.range.start.to_offset(buffer);
+                    let layer_start_point = layer.range.start.to_point(buffer).to_ts_point();
+
+                    info = Some(SyntaxLayerInfo {
                         tree,
                         language,
                         depth: layer.depth,
-                        offset: (
-                            layer.range.start.to_offset(buffer),
-                            layer.range.start.to_point(buffer).to_ts_point(),
-                        ),
-                    };
-                    cursor.next(buffer);
-                    return Some(info);
-                } else {
-                    cursor.next(buffer);
+                        offset: (layer_start_offset, layer_start_point),
+                    });
+                }
+                cursor.next(buffer);
+                if info.is_some() {
+                    return info;
                 }
             }
             None
@@ -1272,14 +1293,20 @@ fn get_injections(
     }
 }
 
+/// Update the given list of included `ranges`, removing any ranges that intersect
+/// `removed_ranges`, and inserting the given `new_ranges`.
+///
+/// Returns a new vector of ranges, and the range of the vector that was changed,
+/// from the previous `ranges` vector.
 pub(crate) fn splice_included_ranges(
     mut ranges: Vec<tree_sitter::Range>,
     removed_ranges: &[Range<usize>],
     new_ranges: &[tree_sitter::Range],
-) -> Vec<tree_sitter::Range> {
+) -> (Vec<tree_sitter::Range>, Range<usize>) {
     let mut removed_ranges = removed_ranges.iter().cloned().peekable();
     let mut new_ranges = new_ranges.into_iter().cloned().peekable();
     let mut ranges_ix = 0;
+    let mut changed_portion = usize::MAX..0;
     loop {
         let next_new_range = new_ranges.peek();
         let next_removed_range = removed_ranges.peek();
@@ -1341,11 +1368,69 @@ pub(crate) fn splice_included_ranges(
             }
         }
 
+        changed_portion.start = changed_portion.start.min(start_ix);
+        changed_portion.end = changed_portion.end.max(if insert.is_some() {
+            start_ix + 1
+        } else {
+            start_ix
+        });
+
         ranges.splice(start_ix..end_ix, insert);
         ranges_ix = start_ix;
     }
 
-    ranges
+    if changed_portion.end < changed_portion.start {
+        changed_portion = 0..0;
+    }
+
+    (ranges, changed_portion)
+}
+
+/// Ensure there are newline ranges in between content range that appear on
+/// different lines. For performance, only iterate through the given range of
+/// indices. All of the ranges in the array are relative to a given start byte
+/// and point.
+fn insert_newlines_between_ranges(
+    indices: Range<usize>,
+    ranges: &mut Vec<tree_sitter::Range>,
+    text: &text::BufferSnapshot,
+    start_byte: usize,
+    start_point: Point,
+) {
+    let mut ix = indices.end + 1;
+    while ix > indices.start {
+        ix -= 1;
+        if 0 == ix || ix == ranges.len() {
+            continue;
+        }
+
+        let range_b = ranges[ix].clone();
+        let range_a = &mut ranges[ix - 1];
+        if range_a.end_point.column == 0 {
+            continue;
+        }
+
+        if range_a.end_point.row < range_b.start_point.row {
+            let end_point = start_point + Point::from_ts_point(range_a.end_point);
+            let line_end = Point::new(end_point.row, text.line_len(end_point.row));
+            if end_point.column as u32 >= line_end.column {
+                range_a.end_byte += 1;
+                range_a.end_point.row += 1;
+                range_a.end_point.column = 0;
+            } else {
+                let newline_offset = text.point_to_offset(line_end);
+                ranges.insert(
+                    ix,
+                    tree_sitter::Range {
+                        start_byte: newline_offset - start_byte,
+                        end_byte: newline_offset - start_byte + 1,
+                        start_point: (line_end - start_point).to_ts_point(),
+                        end_point: ((line_end - start_point) + Point::new(1, 0)).to_ts_point(),
+                    },
+                )
+            }
+        }
+    }
 }
 
 impl OwnedSyntaxLayerInfo {

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -11,7 +11,7 @@ use util::test::marked_text_ranges;
 fn test_splice_included_ranges() {
     let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)];
 
-    let new_ranges = splice_included_ranges(
+    let (new_ranges, change) = splice_included_ranges(
         ranges.clone(),
         &[54..56, 58..68],
         &[ts_range(50..54), ts_range(59..67)],
@@ -25,14 +25,16 @@ fn test_splice_included_ranges() {
             ts_range(80..90),
         ]
     );
+    assert_eq!(change, 1..3);
 
-    let new_ranges = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]);
+    let (new_ranges, change) = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]);
     assert_eq!(
         new_ranges,
         &[ts_range(20..30), ts_range(50..60), ts_range(80..90)]
     );
+    assert_eq!(change, 2..3);
 
-    let new_ranges =
+    let (new_ranges, change) =
         splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]);
     assert_eq!(
         new_ranges,
@@ -44,16 +46,21 @@ fn test_splice_included_ranges() {
             ts_range(80..90)
         ]
     );
+    assert_eq!(change, 0..4);
 
-    let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
+    let (new_ranges, change) =
+        splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
     assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
+    assert_eq!(change, 0..1);
 
     // does not create overlapping ranges
-    let new_ranges = splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
+    let (new_ranges, change) =
+        splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
     assert_eq!(
         new_ranges,
         &[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
     );
+    assert_eq!(change, 0..1);
 
     fn ts_range(range: Range<usize>) -> tree_sitter::Range {
         tree_sitter::Range {
@@ -511,7 +518,7 @@ fn test_removing_injection_by_replacing_across_boundary() {
 }
 
 #[gpui::test]
-fn test_combined_injections() {
+fn test_combined_injections_simple() {
     let (buffer, syntax_map) = test_edit_sequence(
         "ERB",
         &[
@@ -653,33 +660,78 @@ fn test_combined_injections_editing_after_last_injection() {
 
 #[gpui::test]
 fn test_combined_injections_inside_injections() {
-    let (_buffer, _syntax_map) = test_edit_sequence(
+    let (buffer, syntax_map) = test_edit_sequence(
         "Markdown",
         &[
             r#"
-                here is some ERB code:
+                here is
+                some
+                ERB code:
 
                 ```erb
                 <ul>
                 <% people.each do |person| %>
                     <li><%= person.name %></li>
+                    <li><%= person.age %></li>
                 <% end %>
                 </ul>
                 ```
             "#,
             r#"
-                here is some ERB code:
+                here is
+                some
+                ERB code:
 
                 ```erb
                 <ul>
                 <% people«2».each do |person| %>
                     <li><%= person.name %></li>
+                    <li><%= person.age %></li>
+                <% end %>
+                </ul>
+                ```
+            "#,
+            // Inserting a comment character inside one code directive
+            // does not cause the other code directive to become a comment,
+            // because newlines are included in between each injection range.
+            r#"
+                here is
+                some
+                ERB code:
+
+                ```erb
+                <ul>
+                <% people2.each do |person| %>
+                    <li><%= «# »person.name %></li>
+                    <li><%= person.age %></li>
                 <% end %>
                 </ul>
                 ```
             "#,
         ],
     );
+
+    // Check that the code directive below the ruby comment is
+    // not parsed as a comment.
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["method"],
+        "
+            here is
+            some
+            ERB code:
+
+            ```erb
+            <ul>
+            <% people2.«each» do |person| %>
+                <li><%= # person.name %></li>
+                <li><%= person.«age» %></li>
+            <% end %>
+            </ul>
+            ```
+        ",
+    );
 }
 
 #[gpui::test]
@@ -711,11 +763,7 @@ fn test_empty_combined_injections_inside_injections() {
 }
 
 #[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits(mut rng: StdRng) {
-    let operations = env::var("OPERATIONS")
-        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-        .unwrap_or(10);
-
+fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
     let text = r#"
         fn test_something() {
             let vec = vec![5, 1, 3, 8];
@@ -736,68 +784,12 @@ fn test_random_syntax_map_edits(mut rng: StdRng) {
     let registry = Arc::new(LanguageRegistry::test());
     let language = Arc::new(rust_lang());
     registry.add(language.clone());
-    let mut buffer = Buffer::new(0, 0, text);
-
-    let mut syntax_map = SyntaxMap::new();
-    syntax_map.set_language_registry(registry.clone());
-    syntax_map.reparse(language.clone(), &buffer);
-
-    let mut reference_syntax_map = SyntaxMap::new();
-    reference_syntax_map.set_language_registry(registry.clone());
-
-    log::info!("initial text:\n{}", buffer.text());
-
-    for _ in 0..operations {
-        let prev_buffer = buffer.snapshot();
-        let prev_syntax_map = syntax_map.snapshot();
-
-        buffer.randomly_edit(&mut rng, 3);
-        log::info!("text:\n{}", buffer.text());
-
-        syntax_map.interpolate(&buffer);
-        check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer);
-
-        syntax_map.reparse(language.clone(), &buffer);
-
-        reference_syntax_map.clear();
-        reference_syntax_map.reparse(language.clone(), &buffer);
-    }
-
-    for i in 0..operations {
-        let i = operations - i - 1;
-        buffer.undo();
-        log::info!("undoing operation {}", i);
-        log::info!("text:\n{}", buffer.text());
-
-        syntax_map.interpolate(&buffer);
-        syntax_map.reparse(language.clone(), &buffer);
-
-        reference_syntax_map.clear();
-        reference_syntax_map.reparse(language.clone(), &buffer);
-        assert_eq!(
-            syntax_map.layers(&buffer).len(),
-            reference_syntax_map.layers(&buffer).len(),
-            "wrong number of layers after undoing edit {i}"
-        );
-    }
 
-    let layers = syntax_map.layers(&buffer);
-    let reference_layers = reference_syntax_map.layers(&buffer);
-    for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) {
-        assert_eq!(
-            edited_layer.node().to_sexp(),
-            reference_layer.node().to_sexp()
-        );
-        assert_eq!(edited_layer.node().range(), reference_layer.node().range());
-    }
+    test_random_edits(text, registry, language, rng);
 }
 
 #[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) {
-    let operations = env::var("OPERATIONS")
-        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-        .unwrap_or(10);
-
+fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
     let text = r#"
         <div id="main">
         <% if one?(:two) %>
@@ -814,13 +806,60 @@ fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) {
         </div>
     "#
     .unindent()
-    .repeat(8);
+    .repeat(5);
 
     let registry = Arc::new(LanguageRegistry::test());
     let language = Arc::new(erb_lang());
     registry.add(language.clone());
     registry.add(Arc::new(ruby_lang()));
     registry.add(Arc::new(html_lang()));
+
+    test_random_edits(text, registry, language, rng);
+}
+
+#[gpui::test(iterations = 50)]
+fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
+    let text = r#"
+        defmodule TheModule do
+            def the_method(assigns) do
+                ~H"""
+                <%= if @empty do %>
+                    <div class="h-4"></div>
+                <% else %>
+                    <div class="max-w-2xl w-full animate-pulse">
+                    <div class="flex-1 space-y-4">
+                        <div class={[@bg_class, "h-4 rounded-lg w-3/4"]}></div>
+                        <div class={[@bg_class, "h-4 rounded-lg"]}></div>
+                        <div class={[@bg_class, "h-4 rounded-lg w-5/6"]}></div>
+                    </div>
+                    </div>
+                <% end %>
+                """
+            end
+        end
+    "#
+    .unindent()
+    .repeat(3);
+
+    let registry = Arc::new(LanguageRegistry::test());
+    let language = Arc::new(elixir_lang());
+    registry.add(language.clone());
+    registry.add(Arc::new(heex_lang()));
+    registry.add(Arc::new(html_lang()));
+
+    test_random_edits(text, registry, language, rng);
+}
+
+fn test_random_edits(
+    text: String,
+    registry: Arc<LanguageRegistry>,
+    language: Arc<Language>,
+    mut rng: StdRng,
+) {
+    let operations = env::var("OPERATIONS")
+        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+        .unwrap_or(10);
+
     let mut buffer = Buffer::new(0, 0, text);
 
     let mut syntax_map = SyntaxMap::new();
@@ -984,11 +1023,14 @@ fn check_interpolation(
 
 fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
     let registry = Arc::new(LanguageRegistry::test());
+    registry.add(Arc::new(elixir_lang()));
+    registry.add(Arc::new(heex_lang()));
     registry.add(Arc::new(rust_lang()));
     registry.add(Arc::new(ruby_lang()));
     registry.add(Arc::new(html_lang()));
     registry.add(Arc::new(erb_lang()));
     registry.add(Arc::new(markdown_lang()));
+
     let language = registry
         .language_for_name(language_name)
         .now_or_never()
@@ -1074,6 +1116,7 @@ fn ruby_lang() -> Language {
         r#"
             ["if" "do" "else" "end"] @keyword
             (instance_variable) @ivar
+            (call method: (identifier) @method)
         "#,
     )
     .unwrap()
@@ -1158,6 +1201,52 @@ fn markdown_lang() -> Language {
     .unwrap()
 }
 
+fn elixir_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Elixir".into(),
+            path_suffixes: vec!["ex".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_elixir::language()),
+    )
+    .with_highlights_query(
+        r#"
+
+        "#,
+    )
+    .unwrap()
+}
+
+fn heex_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HEEx".into(),
+            path_suffixes: vec!["heex".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_heex::language()),
+    )
+    .with_injection_query(
+        r#"
+        (
+          (directive
+            [
+              (partial_expression_value)
+              (expression_value)
+              (ending_expression_value)
+            ] @content)
+          (#set! language "elixir")
+          (#set! combined)
+        )
+
+        ((expression (expression_value) @content)
+         (#set! language "elixir"))
+        "#,
+    )
+    .unwrap()
+}
+
 fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
     let start = buffer.as_rope().to_string().find(text).unwrap();
     start..start + text.len()

crates/language_selector/src/language_selector.rs 🔗

@@ -93,7 +93,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
             let language_name = &self.candidates[mat.candidate_id].string;
             let language = self.language_registry.language_for_name(language_name);

crates/language_tools/src/lsp_log.rs 🔗

@@ -467,8 +467,13 @@ impl Item for LspLogView {
 impl SearchableItem for LspLogView {
     type Match = <Editor as SearchableItem>::Match;
 
-    fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
-        Editor::to_search_event(event)
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<workspace::searchable::SearchEvent> {
+        self.editor
+            .update(cx, |editor, cx| editor.to_search_event(event, cx))
     }
 
     fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
@@ -494,6 +499,11 @@ impl SearchableItem for LspLogView {
             .update(cx, |e, cx| e.activate_match(index, matches, cx))
     }
 
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |e, cx| e.select_matches(matches, cx))
+    }
+
     fn find_matches(
         &mut self,
         query: project::search::SearchQuery,

crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift 🔗

@@ -6,7 +6,7 @@ import ScreenCaptureKit
 class LKRoomDelegate: RoomDelegate {
     var data: UnsafeRawPointer
     var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
-    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void
     var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
     var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
     var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
@@ -16,7 +16,7 @@ class LKRoomDelegate: RoomDelegate {
     init(
         data: UnsafeRawPointer,
         onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
-        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
         onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
         onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
         onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@@ -43,7 +43,7 @@ class LKRoomDelegate: RoomDelegate {
         if track.kind == .video {
             self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
         } else if track.kind == .audio {
-            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
+            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque())
         }
     }
 
@@ -52,12 +52,12 @@ class LKRoomDelegate: RoomDelegate {
             self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
         }
     }
-    
+
     func room(_ room: Room, didUpdate speakers: [Participant]) {
         guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
         self.onActiveSpeakersChanged(self.data, speaker_ids)
     }
-    
+
     func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
         if track.kind == .video {
             self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
@@ -104,7 +104,7 @@ class LKVideoRenderer: NSObject, VideoRenderer {
 public func LKRoomDelegateCreate(
     data: UnsafeRawPointer,
     onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
-    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
     onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
     onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
     onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@@ -180,39 +180,39 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP
 @_cdecl("LKRoomAudioTracksForRemoteParticipant")
 public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
 @_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
 public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
 @_cdecl("LKRoomVideoTracksForRemoteParticipant")
 public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
@@ -222,7 +222,7 @@ public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
       echoCancellation: true,
       noiseSuppression: true
     ))
-    
+
     return Unmanaged.passRetained(track).toOpaque()
 }
 
@@ -276,7 +276,7 @@ public func LKLocalTrackPublicationSetMute(
     callback_data: UnsafeRawPointer
 ) {
     let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
-    
+
     if muted {
         publication.mute().then {
             on_complete(callback_data, nil)
@@ -307,3 +307,21 @@ public func LKRemoteTrackPublicationSetEnabled(
         on_complete(callback_data, error.localizedDescription as CFString)
     }
 }
+
+@_cdecl("LKRemoteTrackPublicationIsMuted")
+public func LKRemoteTrackPublicationIsMuted(
+    publication: UnsafeRawPointer
+) -> Bool {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.muted
+}
+
+@_cdecl("LKRemoteTrackPublicationGetSid")
+public func LKRemoteTrackPublicationGetSid(
+    publication: UnsafeRawPointer
+) -> CFString {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.sid as CFString
+}

crates/live_kit_client/examples/test_app.rs 🔗

@@ -63,7 +63,7 @@ fn main() {
             let audio_track = LocalAudioTrack::create();
             let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap();
 
-            if let RemoteAudioTrackUpdate::Subscribed(track) =
+            if let RemoteAudioTrackUpdate::Subscribed(track, _) =
                 audio_track_updates.next().await.unwrap()
             {
                 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");

crates/live_kit_client/src/prod.rs 🔗

@@ -26,6 +26,7 @@ extern "C" {
             publisher_id: CFStringRef,
             track_id: CFStringRef,
             remote_track: *const c_void,
+            remote_publication: *const c_void,
         ),
         on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
             callback_data: *mut c_void,
@@ -125,6 +126,9 @@ extern "C" {
         on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
         callback_data: *mut c_void,
     );
+
+    fn LKRemoteTrackPublicationIsMuted(publication: *const c_void) -> bool;
+    fn LKRemoteTrackPublicationGetSid(publication: *const c_void) -> CFStringRef;
 }
 
 pub type Sid = String;
@@ -372,11 +376,19 @@ impl Room {
         rx
     }
 
-    fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) {
+    fn did_subscribe_to_remote_audio_track(
+        &self,
+        track: RemoteAudioTrack,
+        publication: RemoteTrackPublication,
+    ) {
         let track = Arc::new(track);
+        let publication = Arc::new(publication);
         self.remote_audio_track_subscribers.lock().retain(|tx| {
-            tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone()))
-                .is_ok()
+            tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(
+                track.clone(),
+                publication.clone(),
+            ))
+            .is_ok()
         });
     }
 
@@ -501,13 +513,15 @@ impl RoomDelegate {
         publisher_id: CFStringRef,
         track_id: CFStringRef,
         track: *const c_void,
+        publication: *const c_void,
     ) {
         let room = unsafe { Weak::from_raw(room as *mut Room) };
         let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
         let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
         let track = RemoteAudioTrack::new(track, track_id, publisher_id);
+        let publication = RemoteTrackPublication::new(publication);
         if let Some(room) = room.upgrade() {
-            room.did_subscribe_to_remote_audio_track(track);
+            room.did_subscribe_to_remote_audio_track(track, publication);
         }
         let _ = Weak::into_raw(room);
     }
@@ -682,6 +696,14 @@ impl RemoteTrackPublication {
         Self(native_track_publication)
     }
 
+    pub fn sid(&self) -> String {
+        unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
+    }
+
     pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
         let (tx, rx) = futures::channel::oneshot::channel();
 
@@ -832,7 +854,7 @@ pub enum RemoteVideoTrackUpdate {
 pub enum RemoteAudioTrackUpdate {
     ActiveSpeakersChanged { speakers: Vec<Sid> },
     MuteChanged { track_id: Sid, muted: bool },
-    Subscribed(Arc<RemoteAudioTrack>),
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }
 

crates/live_kit_client/src/test.rs 🔗

@@ -216,6 +216,8 @@ impl TestServer {
             publisher_id: identity.clone(),
         });
 
+        let publication = Arc::new(RemoteTrackPublication);
+
         room.audio_tracks.push(track.clone());
 
         for (id, client_room) in &room.client_rooms {
@@ -225,7 +227,10 @@ impl TestServer {
                     .lock()
                     .audio_track_updates
                     .0
-                    .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone()))
+                    .try_broadcast(RemoteAudioTrackUpdate::Subscribed(
+                        track.clone(),
+                        publication.clone(),
+                    ))
                     .unwrap();
             }
         }
@@ -501,6 +506,14 @@ impl RemoteTrackPublication {
     pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
         async { Ok(()) }
     }
+
+    pub fn is_muted(&self) -> bool {
+        false
+    }
+
+    pub fn sid(&self) -> String {
+        "".to_string()
+    }
 }
 
 #[derive(Clone)]
@@ -579,7 +592,7 @@ pub enum RemoteVideoTrackUpdate {
 pub enum RemoteAudioTrackUpdate {
     ActiveSpeakersChanged { speakers: Vec<Sid> },
     MuteChanged { track_id: Sid, muted: bool },
-    Subscribed(Arc<RemoteAudioTrack>),
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }
 

crates/lsp/src/lsp.rs 🔗

@@ -151,16 +151,17 @@ impl LanguageServer {
         let stdin = server.stdin.take().unwrap();
         let stout = server.stdout.take().unwrap();
         let mut server = Self::new_internal(
-            server_id,
+            server_id.clone(),
             stdin,
             stout,
             Some(server),
             root_path,
             code_action_kinds,
             cx,
-            |notification| {
+            move |notification| {
                 log::info!(
-                    "unhandled notification {}:\n{}",
+                    "{} unhandled notification {}:\n{}",
+                    server_id,
                     notification.method,
                     serde_json::to_string_pretty(
                         &notification

crates/menu/src/menu.rs 🔗

@@ -3,6 +3,7 @@ gpui::actions!(
     [
         Cancel,
         Confirm,
+        SecondaryConfirm,
         SelectPrev,
         SelectNext,
         SelectFirst,

crates/node_runtime/src/node_runtime.rs 🔗

@@ -6,13 +6,13 @@ use futures::{future::Shared, FutureExt};
 use gpui::{executor::Background, Task};
 use serde::Deserialize;
 use smol::{fs, io::BufReader, process::Command};
-use std::process::Output;
+use std::process::{Output, Stdio};
 use std::{
     env::consts,
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
-use util::{http::HttpClient, ResultExt};
+use util::http::HttpClient;
 
 const VERSION: &str = "v18.15.0";
 
@@ -62,6 +62,14 @@ impl NodeRuntime {
         args: &[&str],
     ) -> Result<Output> {
         let attempt = |installation_path: PathBuf| async move {
+            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() {
+                    env_path.push(":");
+                    env_path.push(&existing_path);
+                }
+            }
+
             let node_binary = installation_path.join("bin/node");
             let npm_file = installation_path.join("bin/npm");
 
@@ -74,6 +82,7 @@ impl NodeRuntime {
             }
 
             let mut command = Command::new(node_binary);
+            command.env("PATH", env_path);
             command.arg(npm_file).arg(subcommand).args(args);
 
             if let Some(directory) = directory {
@@ -84,9 +93,8 @@ impl NodeRuntime {
         };
 
         let installation_path = self.install_if_needed().await?;
-        let mut output = attempt(installation_path).await;
+        let mut output = attempt(installation_path.clone()).await;
         if output.is_err() {
-            let installation_path = self.reinstall().await?;
             output = attempt(installation_path).await;
             if output.is_err() {
                 return Err(anyhow!(
@@ -158,29 +166,6 @@ impl NodeRuntime {
         Ok(())
     }
 
-    async fn reinstall(&self) -> Result<PathBuf> {
-        log::info!("beginnning to reinstall Node runtime");
-        let mut installation_path = self.installation_path.lock().await;
-
-        if let Some(task) = installation_path.as_ref().cloned() {
-            if let Ok(installation_path) = task.await {
-                smol::fs::remove_dir_all(&installation_path)
-                    .await
-                    .context("node dir removal")
-                    .log_err();
-            }
-        }
-
-        let http = self.http.clone();
-        let task = self
-            .background
-            .spawn(async move { Self::install(http).await.map_err(Arc::new) })
-            .shared();
-
-        *installation_path = Some(task.clone());
-        task.await.map_err(|e| anyhow!("{}", e))
-    }
-
     async fn install_if_needed(&self) -> Result<PathBuf> {
         let task = self
             .installation_path
@@ -209,8 +194,19 @@ impl NodeRuntime {
         let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
         let node_dir = node_containing_dir.join(folder_name);
         let node_binary = node_dir.join("bin/node");
-
-        if fs::metadata(&node_binary).await.is_err() {
+        let npm_file = node_dir.join("bin/npm");
+
+        let result = Command::new(&node_binary)
+            .arg(npm_file)
+            .arg("--version")
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
+            .status()
+            .await;
+        let valid = matches!(result, Ok(status) if status.success());
+
+        if !valid {
             _ = fs::remove_dir_all(&node_containing_dir).await;
             fs::create_dir(&node_containing_dir)
                 .await

crates/outline/src/outline.rs 🔗

@@ -177,7 +177,7 @@ impl PickerDelegate for OutlineViewDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<OutlineView>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<OutlineView>) {
         self.prev_scroll_position.take();
         self.active_editor.update(cx, |active_editor, cx| {
             if let Some(rows) = active_editor.highlighted_rows() {

crates/picker/src/picker.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
     ViewHandle,
 };
-use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
 use parking_lot::Mutex;
 use std::{cmp, sync::Arc};
 use util::ResultExt;
@@ -34,7 +34,7 @@ pub trait PickerDelegate: Sized + 'static {
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>);
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
     fn render_match(
         &self,
@@ -118,8 +118,8 @@ impl<D: PickerDelegate> View for Picker<D> {
                                 // Capture mouse events
                                 .on_down(MouseButton::Left, |_, _, _| {})
                                 .on_up(MouseButton::Left, |_, _, _| {})
-                                .on_click(MouseButton::Left, move |_, picker, cx| {
-                                    picker.select_index(ix, cx);
+                                .on_click(MouseButton::Left, move |click, picker, cx| {
+                                    picker.select_index(ix, click.cmd, cx);
                                 })
                                 .with_cursor_style(CursorStyle::PointingHand)
                                 .into_any()
@@ -175,6 +175,7 @@ impl<D: PickerDelegate> Picker<D> {
         cx.add_action(Self::select_next);
         cx.add_action(Self::select_prev);
         cx.add_action(Self::confirm);
+        cx.add_action(Self::secondary_confirm);
         cx.add_action(Self::cancel);
     }
 
@@ -288,11 +289,11 @@ impl<D: PickerDelegate> Picker<D> {
         cx.notify();
     }
 
-    pub fn select_index(&mut self, index: usize, cx: &mut ViewContext<Self>) {
+    pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext<Self>) {
         if self.delegate.match_count() > 0 {
             self.confirmed = true;
             self.delegate.set_selected_index(index, cx);
-            self.delegate.confirm(cx);
+            self.delegate.confirm(cmd, cx);
         }
     }
 
@@ -330,7 +331,12 @@ impl<D: PickerDelegate> Picker<D> {
 
     pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         self.confirmed = true;
-        self.delegate.confirm(cx);
+        self.delegate.confirm(false, cx);
+    }
+
+    pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        self.confirmed = true;
+        self.delegate.confirm(true, cx);
     }
 
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {

crates/project/src/project.rs 🔗

@@ -259,6 +259,7 @@ pub enum Event {
     LanguageServerLog(LanguageServerId, String),
     Notification(String),
     ActiveEntryChanged(Option<ProjectEntryId>),
+    ActivateProjectPanel,
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
     WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
@@ -425,6 +426,12 @@ pub struct Hover {
     pub language: Option<Arc<Language>>,
 }
 
+impl Hover {
+    pub fn is_empty(&self) -> bool {
+        self.contents.iter().all(|block| block.text.is_empty())
+    }
+}
+
 #[derive(Default)]
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 
@@ -1909,7 +1916,9 @@ impl Project {
                 return;
             }
 
-            let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+            let abs_path = file.abs_path(cx);
+            let uri = lsp::Url::from_file_path(&abs_path)
+                .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}"));
             let initial_snapshot = buffer.text_snapshot();
             let language = buffer.language().cloned();
             let worktree_id = file.worktree_id(cx);
@@ -2709,7 +2718,6 @@ impl Project {
             Some(language_server) => language_server,
             None => return Ok(None),
         };
-
         let this = match this.upgrade(cx) {
             Some(this) => this,
             None => return Err(anyhow!("failed to upgrade project handle")),
@@ -3045,6 +3053,8 @@ impl Project {
     ) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
         let key = (worktree_id, adapter_name);
         if let Some(server_id) = self.language_server_ids.remove(&key) {
+            log::info!("stopping language server {}", key.1 .0);
+
             // Remove other entries for this language server as well
             let mut orphaned_worktrees = vec![worktree_id];
             let other_keys = self.language_server_ids.keys().cloned().collect::<Vec<_>>();

crates/project/src/worktree.rs 🔗

@@ -397,6 +397,7 @@ impl Worktree {
         }))
     }
 
+    // abcdefghi
     pub fn remote(
         project_remote_id: u64,
         replica_id: ReplicaId,
@@ -2014,36 +2015,6 @@ impl LocalSnapshot {
         entry
     }
 
-    #[must_use = "Changed paths must be used for diffing later"]
-    fn scan_statuses(
-        &mut self,
-        repo_ptr: &dyn GitRepository,
-        work_directory: &RepositoryWorkDirectory,
-    ) -> Vec<Arc<Path>> {
-        let mut changes = vec![];
-        let mut edits = vec![];
-        for mut entry in self
-            .descendent_entries(false, false, &work_directory.0)
-            .cloned()
-        {
-            let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
-                continue;
-            };
-            let git_file_status = repo_ptr
-                .status(&RepoPath(repo_path.into()))
-                .log_err()
-                .flatten();
-            if entry.git_status != git_file_status {
-                entry.git_status = git_file_status;
-                changes.push(entry.path.clone());
-                edits.push(Edit::Insert(entry));
-            }
-        }
-
-        self.entries_by_path.edit(edits, &());
-        changes
-    }
-
     fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
         let mut inodes = TreeSet::default();
         for ancestor in path.ancestors().skip(1) {
@@ -2187,6 +2158,38 @@ impl BackgroundScannerState {
                 .any(|p| entry.path.starts_with(p))
     }
 
+    fn enqueue_scan_dir(&self, abs_path: Arc<Path>, entry: &Entry, scan_job_tx: &Sender<ScanJob>) {
+        let path = entry.path.clone();
+        let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
+        let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
+        let mut containing_repository = None;
+        if !ignore_stack.is_all() {
+            if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
+                if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
+                    containing_repository = Some((
+                        workdir_path,
+                        repo.repo_ptr.clone(),
+                        repo.repo_ptr.lock().staged_statuses(repo_path),
+                    ));
+                }
+            }
+        }
+        if !ancestor_inodes.contains(&entry.inode) {
+            ancestor_inodes.insert(entry.inode);
+            scan_job_tx
+                .try_send(ScanJob {
+                    abs_path,
+                    path,
+                    ignore_stack,
+                    scan_queue: scan_job_tx.clone(),
+                    ancestor_inodes,
+                    is_external: entry.is_external,
+                    containing_repository,
+                })
+                .unwrap();
+        }
+    }
+
     fn reuse_entry_id(&mut self, entry: &mut Entry) {
         if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
             entry.id = removed_entry_id;
@@ -2199,7 +2202,7 @@ impl BackgroundScannerState {
         self.reuse_entry_id(&mut entry);
         let entry = self.snapshot.insert_entry(entry, fs);
         if entry.path.file_name() == Some(&DOT_GIT) {
-            self.build_repository(entry.path.clone(), fs);
+            self.build_git_repository(entry.path.clone(), fs);
         }
 
         #[cfg(test)]
@@ -2213,7 +2216,6 @@ impl BackgroundScannerState {
         parent_path: &Arc<Path>,
         entries: impl IntoIterator<Item = Entry>,
         ignore: Option<Arc<Gitignore>>,
-        fs: &dyn Fs,
     ) {
         let mut parent_entry = if let Some(parent_entry) = self
             .snapshot
@@ -2242,16 +2244,12 @@ impl BackgroundScannerState {
                 .insert(abs_parent_path, (ignore, false));
         }
 
-        self.scanned_dirs.insert(parent_entry.id);
+        let parent_entry_id = parent_entry.id;
+        self.scanned_dirs.insert(parent_entry_id);
         let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
         let mut entries_by_id_edits = Vec::new();
-        let mut dotgit_path = None;
 
         for entry in entries {
-            if entry.path.file_name() == Some(&DOT_GIT) {
-                dotgit_path = Some(entry.path.clone());
-            }
-
             entries_by_id_edits.push(Edit::Insert(PathEntry {
                 id: entry.id,
                 path: entry.path.clone(),
@@ -2266,9 +2264,6 @@ impl BackgroundScannerState {
             .edit(entries_by_path_edits, &());
         self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
 
-        if let Some(dotgit_path) = dotgit_path {
-            self.build_repository(dotgit_path, fs);
-        }
         if let Err(ix) = self.changed_paths.binary_search(parent_path) {
             self.changed_paths.insert(ix, parent_path.clone());
         }
@@ -2344,7 +2339,7 @@ impl BackgroundScannerState {
                 });
             match repository {
                 None => {
-                    self.build_repository(dot_git_dir.into(), fs);
+                    self.build_git_repository(dot_git_dir.into(), fs);
                 }
                 Some((entry_id, repository)) => {
                     if repository.git_dir_scan_id == scan_id {
@@ -2368,13 +2363,7 @@ impl BackgroundScannerState {
                         .repository_entries
                         .update(&work_dir, |entry| entry.branch = branch.map(Into::into));
 
-                    let changed_paths = self.snapshot.scan_statuses(&*repository, &work_dir);
-                    util::extend_sorted(
-                        &mut self.changed_paths,
-                        changed_paths,
-                        usize::MAX,
-                        Ord::cmp,
-                    )
+                    self.update_git_statuses(&work_dir, &*repository);
                 }
             }
         }
@@ -2395,7 +2384,15 @@ impl BackgroundScannerState {
         snapshot.repository_entries = repository_entries;
     }
 
-    fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
+    fn build_git_repository(
+        &mut self,
+        dot_git_path: Arc<Path>,
+        fs: &dyn Fs,
+    ) -> Option<(
+        RepositoryWorkDirectory,
+        Arc<Mutex<dyn GitRepository>>,
+        TreeMap<RepoPath, GitFileStatus>,
+    )> {
         log::info!("build git repository {:?}", dot_git_path);
 
         let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
@@ -2427,22 +2424,54 @@ impl BackgroundScannerState {
             },
         );
 
-        let changed_paths = self
-            .snapshot
-            .scan_statuses(repo_lock.deref(), &work_directory);
+        let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock);
         drop(repo_lock);
 
         self.snapshot.git_repositories.insert(
             work_dir_id,
             LocalRepositoryEntry {
                 git_dir_scan_id: 0,
-                repo_ptr: repository,
+                repo_ptr: repository.clone(),
                 git_dir_path: dot_git_path.clone(),
             },
         );
 
-        util::extend_sorted(&mut self.changed_paths, changed_paths, usize::MAX, Ord::cmp);
-        Some(())
+        Some((work_directory, repository, staged_statuses))
+    }
+
+    fn update_git_statuses(
+        &mut self,
+        work_directory: &RepositoryWorkDirectory,
+        repo: &dyn GitRepository,
+    ) -> TreeMap<RepoPath, GitFileStatus> {
+        let staged_statuses = repo.staged_statuses(Path::new(""));
+
+        let mut changes = vec![];
+        let mut edits = vec![];
+
+        for mut entry in self
+            .snapshot
+            .descendent_entries(false, false, &work_directory.0)
+            .cloned()
+        {
+            let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
+                continue;
+            };
+            let repo_path = RepoPath(repo_path.to_path_buf());
+            let git_file_status = combine_git_statuses(
+                staged_statuses.get(&repo_path).copied(),
+                repo.unstaged_status(&repo_path, entry.mtime),
+            );
+            if entry.git_status != git_file_status {
+                entry.git_status = git_file_status;
+                changes.push(entry.path.clone());
+                edits.push(Edit::Insert(entry));
+            }
+        }
+
+        self.snapshot.entries_by_path.edit(edits, &());
+        util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp);
+        staged_statuses
     }
 }
 
@@ -3029,16 +3058,8 @@ impl BackgroundScanner {
     ) {
         use futures::FutureExt as _;
 
-        let (root_abs_path, root_inode) = {
-            let snapshot = &self.state.lock().snapshot;
-            (
-                snapshot.abs_path.clone(),
-                snapshot.root_entry().map(|e| e.inode),
-            )
-        };
-
         // Populate ignores above the root.
-        let ignore_stack;
+        let root_abs_path = self.state.lock().snapshot.abs_path.clone();
         for ancestor in root_abs_path.ancestors().skip(1) {
             if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
             {
@@ -3049,31 +3070,24 @@ impl BackgroundScanner {
                     .insert(ancestor.into(), (ignore.into(), false));
             }
         }
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
         {
             let mut state = self.state.lock();
             state.snapshot.scan_id += 1;
-            ignore_stack = state
-                .snapshot
-                .ignore_stack_for_abs_path(&root_abs_path, true);
-            if ignore_stack.is_all() {
-                if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
+            if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
+                let ignore_stack = state
+                    .snapshot
+                    .ignore_stack_for_abs_path(&root_abs_path, true);
+                if ignore_stack.is_all() {
                     root_entry.is_ignored = true;
-                    state.insert_entry(root_entry, self.fs.as_ref());
+                    state.insert_entry(root_entry.clone(), self.fs.as_ref());
                 }
+                state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
             }
         };
 
         // Perform an initial scan of the directory.
-        let (scan_job_tx, scan_job_rx) = channel::unbounded();
-        smol::block_on(scan_job_tx.send(ScanJob {
-            abs_path: root_abs_path,
-            path: Arc::from(Path::new("")),
-            ignore_stack,
-            ancestor_inodes: TreeSet::from_ordered_entries(root_inode),
-            is_external: false,
-            scan_queue: scan_job_tx.clone(),
-        }))
-        .unwrap();
         drop(scan_job_tx);
         self.scan_dirs(true, scan_job_rx).await;
         {
@@ -3261,20 +3275,7 @@ impl BackgroundScanner {
                     if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
                         if entry.kind == EntryKind::UnloadedDir {
                             let abs_path = root_path.join(ancestor);
-                            let ignore_stack =
-                                state.snapshot.ignore_stack_for_abs_path(&abs_path, true);
-                            let ancestor_inodes =
-                                state.snapshot.ancestor_inodes_for_path(&ancestor);
-                            scan_job_tx
-                                .try_send(ScanJob {
-                                    abs_path: abs_path.into(),
-                                    path: ancestor.into(),
-                                    ignore_stack,
-                                    scan_queue: scan_job_tx.clone(),
-                                    ancestor_inodes,
-                                    is_external: entry.is_external,
-                                })
-                                .unwrap();
+                            state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
                             state.paths_to_scan.insert(path.clone());
                             break;
                         }
@@ -3389,18 +3390,16 @@ impl BackgroundScanner {
 
         let mut ignore_stack = job.ignore_stack.clone();
         let mut new_ignore = None;
-        let (root_abs_path, root_char_bag, next_entry_id, repository) = {
+        let (root_abs_path, root_char_bag, next_entry_id) = {
             let snapshot = &self.state.lock().snapshot;
             (
                 snapshot.abs_path().clone(),
                 snapshot.root_char_bag,
                 self.next_entry_id.clone(),
-                snapshot
-                    .local_repo_for_path(&job.path)
-                    .map(|(work_dir, repo)| (work_dir, repo.clone())),
             )
         };
 
+        let mut dotgit_path = None;
         let mut root_canonical_path = None;
         let mut new_entries: Vec<Entry> = Vec::new();
         let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
@@ -3463,6 +3462,10 @@ impl BackgroundScanner {
                     }
                 }
             }
+            // If we find a .git, we'll need to load the repository.
+            else if child_name == *DOT_GIT {
+                dotgit_path = Some(child_path.clone());
+            }
 
             let mut child_entry = Entry::new(
                 child_path.clone(),
@@ -3523,6 +3526,7 @@ impl BackgroundScanner {
                         },
                         ancestor_inodes,
                         scan_queue: job.scan_queue.clone(),
+                        containing_repository: job.containing_repository.clone(),
                     }));
                 } else {
                     new_jobs.push(None);
@@ -3530,14 +3534,17 @@ impl BackgroundScanner {
             } else {
                 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
                 if !child_entry.is_ignored {
-                    if let Some((repo_path, repo)) = &repository {
-                        if let Ok(path) = child_path.strip_prefix(&repo_path.0) {
-                            child_entry.git_status = repo
-                                .repo_ptr
-                                .lock()
-                                .status(&RepoPath(path.into()))
-                                .log_err()
-                                .flatten();
+                    if let Some((repository_dir, repository, staged_statuses)) =
+                        &job.containing_repository
+                    {
+                        if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) {
+                            let repo_path = RepoPath(repo_path.into());
+                            child_entry.git_status = combine_git_statuses(
+                                staged_statuses.get(&repo_path).copied(),
+                                repository
+                                    .lock()
+                                    .unstaged_status(&repo_path, child_entry.mtime),
+                            );
                         }
                     }
                 }
@@ -3547,27 +3554,39 @@ impl BackgroundScanner {
         }
 
         let mut state = self.state.lock();
-        let mut new_jobs = new_jobs.into_iter();
+
+        // Identify any subdirectories that should not be scanned.
+        let mut job_ix = 0;
         for entry in &mut new_entries {
             state.reuse_entry_id(entry);
-
             if entry.is_dir() {
-                let new_job = new_jobs.next().expect("missing scan job for entry");
                 if state.should_scan_directory(&entry) {
-                    if let Some(new_job) = new_job {
-                        job.scan_queue
-                            .try_send(new_job)
-                            .expect("channel is unbounded");
-                    }
+                    job_ix += 1;
                 } else {
                     log::debug!("defer scanning directory {:?}", entry.path);
                     entry.kind = EntryKind::UnloadedDir;
+                    new_jobs.remove(job_ix);
+                }
+            }
+        }
+
+        state.populate_dir(&job.path, new_entries, new_ignore);
+
+        let repository =
+            dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref()));
+
+        for new_job in new_jobs {
+            if let Some(mut new_job) = new_job {
+                if let Some(containing_repository) = &repository {
+                    new_job.containing_repository = Some(containing_repository.clone());
                 }
+
+                job.scan_queue
+                    .try_send(new_job)
+                    .expect("channel is unbounded");
             }
         }
-        assert!(new_jobs.next().is_none());
 
-        state.populate_dir(&job.path, new_entries, new_ignore, self.fs.as_ref());
         Ok(())
     }
 
@@ -3636,13 +3655,10 @@ impl BackgroundScanner {
                             if let Some((work_dir, repo)) =
                                 state.snapshot.local_repo_for_path(&path)
                             {
-                                if let Ok(path) = path.strip_prefix(work_dir.0) {
-                                    fs_entry.git_status = repo
-                                        .repo_ptr
-                                        .lock()
-                                        .status(&RepoPath(path.into()))
-                                        .log_err()
-                                        .flatten()
+                                if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
+                                    let repo_path = RepoPath(repo_path.into());
+                                    let repo = repo.repo_ptr.lock();
+                                    fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
                                 }
                             }
                         }
@@ -3650,20 +3666,7 @@ impl BackgroundScanner {
 
                     if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
                         if state.should_scan_directory(&fs_entry) {
-                            let mut ancestor_inodes =
-                                state.snapshot.ancestor_inodes_for_path(&path);
-                            if !ancestor_inodes.contains(&metadata.inode) {
-                                ancestor_inodes.insert(metadata.inode);
-                                smol::block_on(scan_queue_tx.send(ScanJob {
-                                    abs_path,
-                                    path: path.clone(),
-                                    ignore_stack,
-                                    ancestor_inodes,
-                                    is_external: fs_entry.is_external,
-                                    scan_queue: scan_queue_tx.clone(),
-                                }))
-                                .unwrap();
-                            }
+                            state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx);
                         } else {
                             fs_entry.kind = EntryKind::UnloadedDir;
                         }
@@ -3820,18 +3823,7 @@ impl BackgroundScanner {
                 if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
                     let state = self.state.lock();
                     if state.should_scan_directory(&entry) {
-                        job.scan_queue
-                            .try_send(ScanJob {
-                                abs_path: abs_path.clone(),
-                                path: entry.path.clone(),
-                                ignore_stack: child_ignore_stack.clone(),
-                                scan_queue: job.scan_queue.clone(),
-                                ancestor_inodes: state
-                                    .snapshot
-                                    .ancestor_inodes_for_path(&entry.path),
-                                is_external: false,
-                            })
-                            .unwrap();
+                        state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue);
                     }
                 }
 
@@ -4020,6 +4012,11 @@ struct ScanJob {
     scan_queue: Sender<ScanJob>,
     ancestor_inodes: TreeSet<u64>,
     is_external: bool,
+    containing_repository: Option<(
+        RepositoryWorkDirectory,
+        Arc<Mutex<dyn GitRepository>>,
+        TreeMap<RepoPath, GitFileStatus>,
+    )>,
 }
 
 struct UpdateIgnoreStatusJob {
@@ -4346,3 +4343,22 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
         }
     }
 }
+
+fn combine_git_statuses(
+    staged: Option<GitFileStatus>,
+    unstaged: Option<GitFileStatus>,
+) -> Option<GitFileStatus> {
+    if let Some(staged) = staged {
+        if let Some(unstaged) = unstaged {
+            if unstaged != staged {
+                Some(GitFileStatus::Modified)
+            } else {
+                Some(staged)
+            }
+        } else {
+            Some(staged)
+        }
+    } else {
+        unstaged
+    }
+}

crates/project_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 context_menu = { path = "../context_menu" }
+collections = { path = "../collections" }
 db = { path = "../db" }
 drag_and_drop = { path = "../drag_and_drop" }
 editor = { path = "../editor" }

crates/project_panel/src/file_associations.rs 🔗

@@ -0,0 +1,103 @@
+use std::{path::Path, str, sync::Arc};
+
+use collections::HashMap;
+
+use gpui::{AppContext, AssetSource};
+use serde_derive::Deserialize;
+use util::iife;
+
+#[derive(Deserialize, Debug)]
+struct TypeConfig {
+    icon: Arc<str>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct FileAssociations {
+    suffixes: HashMap<String, String>,
+    types: HashMap<String, TypeConfig>,
+}
+
+const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
+const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
+const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
+const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
+pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
+
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(FileAssociations::new(assets))
+}
+
+impl FileAssociations {
+    pub fn new(assets: impl AssetSource) -> Self {
+        assets
+            .load("icons/file_icons/file_types.json")
+            .and_then(|file| {
+                serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
+                    .map_err(Into::into)
+            })
+            .unwrap_or_else(|_| FileAssociations {
+                suffixes: HashMap::default(),
+                types: HashMap::default(),
+            })
+    }
+
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
+        iife!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            // 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..))
+                    })?;
+
+                this.suffixes
+                    .get(suffix)
+                    .and_then(|type_str| this.types.get(type_str))
+                    .map(|type_config| type_config.icon.clone())
+            })
+            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        iife!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            let key = if expanded {
+                EXPANDED_DIRECTORY_TYPE
+            } else {
+                COLLAPSED_DIRECTORY_TYPE
+            };
+
+            this.types
+                .get(key)
+                .map(|type_config| type_config.icon.clone())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        iife!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            let key = if expanded {
+                EXPANDED_CHEVRON_TYPE
+            } else {
+                COLLAPSED_CHEVRON_TYPE
+            };
+
+            this.types
+                .get(key)
+                .map(|type_config| type_config.icon.clone())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+}

crates/project_panel/src/project_panel.rs 🔗

@@ -1,9 +1,12 @@
+pub mod file_associations;
 mod project_panel_settings;
 
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
 use drag_and_drop::{DragAndDrop, Draggable};
-use editor::{Cancel, Editor};
+use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
+use file_associations::FileAssociations;
+
 use futures::stream::StreamExt;
 use gpui::{
     actions,
@@ -15,8 +18,8 @@ use gpui::{
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle,
-    Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity,
+    ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -94,6 +97,7 @@ pub enum ClipboardEntry {
 #[derive(Debug, PartialEq, Eq)]
 pub struct EntryDetails {
     filename: String,
+    icon: Option<Arc<str>>,
     path: Arc<Path>,
     depth: usize,
     kind: EntryKind,
@@ -121,7 +125,9 @@ actions!(
         Paste,
         Delete,
         Rename,
-        ToggleFocus
+        Open,
+        ToggleFocus,
+        NewSearchInDirectory,
     ]
 );
 
@@ -129,8 +135,9 @@ pub fn init_settings(cx: &mut AppContext) {
     settings::register::<ProjectPanelSettings>(cx);
 }
 
-pub fn init(cx: &mut AppContext) {
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     init_settings(cx);
+    file_associations::init(assets, cx);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
     cx.add_action(ProjectPanel::select_prev);
@@ -140,12 +147,14 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectPanel::rename);
     cx.add_async_action(ProjectPanel::delete);
     cx.add_async_action(ProjectPanel::confirm);
+    cx.add_async_action(ProjectPanel::open_file);
     cx.add_action(ProjectPanel::cancel);
     cx.add_action(ProjectPanel::cut);
     cx.add_action(ProjectPanel::copy);
     cx.add_action(ProjectPanel::copy_path);
     cx.add_action(ProjectPanel::copy_relative_path);
     cx.add_action(ProjectPanel::reveal_in_finder);
+    cx.add_action(ProjectPanel::new_search_in_directory);
     cx.add_action(
         |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
             this.paste(action, cx);
@@ -159,8 +168,15 @@ pub enum Event {
         entry_id: ProjectEntryId,
         focus_opened_item: bool,
     },
+    SplitEntry {
+        entry_id: ProjectEntryId,
+    },
     DockPositionChanged,
     Focus,
+    NewSearchInDirectory {
+        dir_entry: Entry,
+    },
+    ActivatePanel,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -187,6 +203,9 @@ impl ProjectPanel {
                         cx.notify();
                     }
                 }
+                project::Event::ActivateProjectPanel => {
+                    cx.emit(Event::ActivatePanel);
+                }
                 project::Event::WorktreeRemoved(id) => {
                     this.expanded_dir_ids.remove(id);
                     this.update_visible_entries(None, cx);
@@ -227,6 +246,11 @@ impl ProjectPanel {
             })
             .detach();
 
+            cx.observe_global::<FileAssociations, _>(|_, cx| {
+                cx.notify();
+            })
+            .detach();
+
             let view_id = cx.view_id();
             let mut this = Self {
                 project: project.clone(),
@@ -290,6 +314,21 @@ impl ProjectPanel {
                         }
                     }
                 }
+                &Event::SplitEntry { entry_id } => {
+                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            workspace
+                                .split_path(
+                                    ProjectPath {
+                                        worktree_id: worktree.read(cx).id(),
+                                        path: entry.path.clone(),
+                                    },
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                        }
+                    }
+                }
                 _ => {}
             }
         })
@@ -389,6 +428,12 @@ impl ProjectPanel {
                 CopyRelativePath,
             ));
             menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
+            if entry.is_dir() {
+                menu_entries.push(ContextMenuItem::action(
+                    "Search inside",
+                    NewSearchInDirectory,
+                ));
+            }
             if let Some(clipboard_entry) = self.clipboard_entry {
                 if clipboard_entry.worktree_id() == worktree.id() {
                     menu_entries.push(ContextMenuItem::action("Paste", Paste));
@@ -517,15 +562,20 @@ impl ProjectPanel {
 
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
         if let Some(task) = self.confirm_edit(cx) {
-            Some(task)
-        } else if let Some((_, entry)) = self.selected_entry(cx) {
+            return Some(task);
+        }
+
+        None
+    }
+
+    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        if let Some((_, entry)) = self.selected_entry(cx) {
             if entry.is_file() {
                 self.open_entry(entry.id, true, cx);
             }
-            None
-        } else {
-            None
         }
+
+        None
     }
 
     fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@@ -620,6 +670,10 @@ impl ProjectPanel {
         });
     }
 
+    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::SplitEntry { entry_id });
+    }
+
     fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
         self.add_entry(false, cx)
     }
@@ -698,13 +752,20 @@ impl ProjectPanel {
                         is_dir: entry.is_dir(),
                         processing_filename: None,
                     });
-                    let filename = entry
+                    let file_name = entry
                         .path
                         .file_name()
-                        .map_or(String::new(), |s| s.to_string_lossy().to_string());
+                        .map(|s| s.to_string_lossy())
+                        .unwrap_or_default()
+                        .to_string();
+                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
+                    let selection_end =
+                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
                     self.filename_editor.update(cx, |editor, cx| {
-                        editor.set_text(filename, cx);
-                        editor.select_all(&Default::default(), cx);
+                        editor.set_text(file_name, cx);
+                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                            s.select_ranges([0..selection_end])
+                        })
                     });
                     cx.focus(&self.filename_editor);
                     self.update_visible_entries(None, cx);
@@ -896,6 +957,20 @@ impl ProjectPanel {
         }
     }
 
+    pub fn new_search_in_directory(
+        &mut self,
+        _: &NewSearchInDirectory,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some((_, entry)) = self.selected_entry(cx) {
+            if entry.is_dir() {
+                cx.emit(Event::NewSearchInDirectory {
+                    dir_entry: entry.clone(),
+                });
+            }
+        }
+    }
+
     fn move_entry(
         &mut self,
         entry_to_move: ProjectEntryId,
@@ -950,7 +1025,10 @@ impl ProjectPanel {
         None
     }
 
-    fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
+    pub fn selected_entry<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> Option<(&'a Worktree, &'a project::Entry)> {
         let (worktree, entry) = self.selected_entry_handle(cx)?;
         Some((worktree.read(cx), entry))
     }
@@ -1144,7 +1222,14 @@ impl ProjectPanel {
             }
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
-            let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
+            let (git_status_setting, show_file_icons, show_folder_icons) = {
+                let settings = settings::get::<ProjectPanelSettings>(cx);
+                (
+                    settings.git_status,
+                    settings.file_icons,
+                    settings.folder_icons,
+                )
+            };
             if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
                 let snapshot = worktree.read(cx).snapshot();
                 let root_name = OsStr::new(snapshot.root_name());
@@ -1157,6 +1242,23 @@ impl ProjectPanel {
                 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
                 for entry in visible_worktree_entries[entry_range].iter() {
                     let status = git_status_setting.then(|| entry.git_status).flatten();
+                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
+                    let icon = match entry.kind {
+                        EntryKind::File(_) => {
+                            if show_file_icons {
+                                Some(FileAssociations::get_icon(&entry.path, cx))
+                            } else {
+                                None
+                            }
+                        }
+                        _ => {
+                            if show_folder_icons {
+                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
+                            } else {
+                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
+                            }
+                        }
+                    };
 
                     let mut details = EntryDetails {
                         filename: entry
@@ -1165,11 +1267,12 @@ impl ProjectPanel {
                             .unwrap_or(root_name)
                             .to_string_lossy()
                             .to_string(),
+                        icon,
                         path: entry.path.clone(),
                         depth: entry.path.components().count(),
                         kind: entry.kind,
                         is_ignored: entry.is_ignored,
-                        is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
+                        is_expanded,
                         is_selected: self.selection.map_or(false, |e| {
                             e.worktree_id == snapshot.id() && e.entry_id == entry.id
                         }),
@@ -1217,7 +1320,6 @@ impl ProjectPanel {
         style: &ProjectPanelEntry,
         cx: &mut ViewContext<V>,
     ) -> AnyElement<V> {
-        let kind = details.kind;
         let show_editor = details.is_editing && !details.is_processing;
 
         let mut filename_text_style = style.text.clone();
@@ -1232,23 +1334,24 @@ impl ProjectPanel {
             .unwrap_or(style.text.color);
 
         Flex::row()
-            .with_child(
-                if kind.is_dir() {
-                    if details.is_expanded {
-                        Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
-                    } else {
-                        Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
-                    }
+            .with_child(if let Some(icon) = &details.icon {
+                Svg::new(icon.to_string())
+                    .with_color(style.icon_color)
                     .constrained()
-                } else {
-                    Empty::new().constrained()
-                }
-                .with_max_width(style.icon_size)
-                .with_max_height(style.icon_size)
-                .aligned()
-                .constrained()
-                .with_width(style.icon_size),
-            )
+                    .with_max_width(style.icon_size)
+                    .with_max_height(style.icon_size)
+                    .aligned()
+                    .constrained()
+                    .with_width(style.icon_size)
+            } else {
+                Empty::new()
+                    .constrained()
+                    .with_max_width(style.icon_size)
+                    .with_max_height(style.icon_size)
+                    .aligned()
+                    .constrained()
+                    .with_width(style.icon_size)
+            })
             .with_child(if show_editor && editor.is_some() {
                 ChildView::new(editor.as_ref().unwrap(), cx)
                     .contained()
@@ -1283,7 +1386,8 @@ impl ProjectPanel {
     ) -> AnyElement<Self> {
         let kind = details.kind;
         let path = details.path.clone();
-        let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
+        let settings = settings::get::<ProjectPanelSettings>(cx);
+        let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size;
 
         let entry_style = if details.is_cut {
             &theme.cut_entry
@@ -1333,7 +1437,11 @@ impl ProjectPanel {
                 if kind.is_dir() {
                     this.toggle_expanded(entry_id, cx);
                 } else {
-                    this.open_entry(entry_id, event.click_count > 1, cx);
+                    if event.cmd {
+                        this.split_entry(entry_id, cx);
+                    } else if !event.cmd {
+                        this.open_entry(entry_id, event.click_count > 1, cx);
+                    }
                 }
             }
         })
@@ -1615,7 +1723,11 @@ mod tests {
     use project::FakeFs;
     use serde_json::json;
     use settings::SettingsStore;
-    use std::{collections::HashSet, path::Path};
+    use std::{
+        collections::HashSet,
+        path::Path,
+        sync::atomic::{self, AtomicUsize},
+    };
     use workspace::{pane, AppState};
 
     #[gpui::test]
@@ -1851,7 +1963,7 @@ mod tests {
             .update(cx, |panel, cx| {
                 panel
                     .filename_editor
-                    .update(cx, |editor, cx| editor.set_text("another-filename", cx));
+                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
                 panel.confirm(&Confirm, cx).unwrap()
             })
             .await
@@ -1865,14 +1977,14 @@ mod tests {
                 "    v b",
                 "        > 3",
                 "        > 4",
-                "          another-filename  <== selected",
+                "          another-filename.txt  <== selected",
                 "    > C",
                 "      .dockerignore",
                 "      the-new-filename",
             ]
         );
 
-        select_path(&panel, "root1/b/another-filename", cx);
+        select_path(&panel, "root1/b/another-filename.txt", cx);
         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
@@ -1883,7 +1995,7 @@ mod tests {
                 "    v b",
                 "        > 3",
                 "        > 4",
-                "          [EDITOR: 'another-filename']  <== selected",
+                "          [EDITOR: 'another-filename.txt']  <== selected",
                 "    > C",
                 "      .dockerignore",
                 "      the-new-filename",
@@ -1891,9 +2003,15 @@ mod tests {
         );
 
         let confirm = panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("a-different-filename", cx));
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
+
+                editor.set_text("a-different-filename.tar.gz", cx)
+            });
             panel.confirm(&Confirm, cx).unwrap()
         });
         assert_eq!(
@@ -1905,7 +2023,7 @@ mod tests {
                 "    v b",
                 "        > 3",
                 "        > 4",
-                "          [PROCESSING: 'a-different-filename']  <== selected",
+                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
                 "    > C",
                 "      .dockerignore",
                 "      the-new-filename",
@@ -1922,13 +2040,42 @@ mod tests {
                 "    v b",
                 "        > 3",
                 "        > 4",
-                "          a-different-filename  <== selected",
+                "          a-different-filename.tar.gz  <== selected",
                 "    > C",
                 "      .dockerignore",
                 "      the-new-filename",
             ]
         );
 
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot");
+
+            });
+            panel.cancel(&Cancel, cx)
+        });
+
         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
@@ -1940,7 +2087,7 @@ mod tests {
                 "        > [EDITOR: '']  <== selected",
                 "        > 3",
                 "        > 4",
-                "          a-different-filename",
+                "          a-different-filename.tar.gz",
                 "    > C",
                 "      .dockerignore",
             ]
@@ -1963,7 +2110,7 @@ mod tests {
                 "        > [PROCESSING: 'new-dir']",
                 "        > 3  <== selected",
                 "        > 4",
-                "          a-different-filename",
+                "          a-different-filename.tar.gz",
                 "    > C",
                 "      .dockerignore",
             ]
@@ -1980,7 +2127,7 @@ mod tests {
                 "        > 3  <== selected",
                 "        > 4",
                 "        > new-dir",
-                "          a-different-filename",
+                "          a-different-filename.tar.gz",
                 "    > C",
                 "      .dockerignore",
             ]
@@ -1997,7 +2144,7 @@ mod tests {
                 "        > [EDITOR: '3']  <== selected",
                 "        > 4",
                 "        > new-dir",
-                "          a-different-filename",
+                "          a-different-filename.tar.gz",
                 "    > C",
                 "      .dockerignore",
             ]
@@ -2015,7 +2162,7 @@ mod tests {
                 "        > 3  <== selected",
                 "        > 4",
                 "        > new-dir",
-                "          a-different-filename",
+                "          a-different-filename.tar.gz",
                 "    > C",
                 "      .dockerignore",
             ]
@@ -2242,7 +2389,7 @@ mod tests {
 
         toggle_expand_dir(&panel, "src/test", cx);
         select_path(&panel, "src/test/first.rs", cx);
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
         cx.foreground().run_until_parked();
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
@@ -2270,7 +2417,7 @@ mod tests {
         ensure_no_open_items_and_panes(window_id, &workspace, cx);
 
         select_path(&panel, "src/test/second.rs", cx);
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
         cx.foreground().run_until_parked();
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
@@ -2454,6 +2601,83 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        let new_search_events_count = Arc::new(AtomicUsize::new(0));
+        let _subscription = panel.update(cx, |_, cx| {
+            let subcription_count = Arc::clone(&new_search_events_count);
+            cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                if matches!(event, Event::NewSearchInDirectory { .. }) {
+                    subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
+                }
+            })
+        });
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            0,
+            "Should not trigger new search in directory when called on a file"
+        );
+
+        select_path(&panel, "src/test", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            1,
+            "Should trigger new search in directory when called on a directory"
+        );
+    }
+
     fn toggle_expand_dir(
         panel: &ViewHandle<ProjectPanel>,
         path: impl AsRef<Path>,
@@ -2555,7 +2779,7 @@ mod tests {
             theme::init((), cx);
             language::init(cx);
             editor::init_settings(cx);
-            crate::init(cx);
+            crate::init((), cx);
             workspace::init_settings(cx);
             Project::init_settings(cx);
         });
@@ -2570,7 +2794,7 @@ mod tests {
             language::init(cx);
             editor::init(cx);
             pane::init(cx);
-            crate::init(cx);
+            crate::init((), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);
         });

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -12,16 +12,22 @@ pub enum ProjectPanelDockPosition {
 
 #[derive(Deserialize, Debug)]
 pub struct ProjectPanelSettings {
-    pub git_status: bool,
-    pub dock: ProjectPanelDockPosition,
     pub default_width: f32,
+    pub dock: ProjectPanelDockPosition,
+    pub file_icons: bool,
+    pub folder_icons: bool,
+    pub git_status: bool,
+    pub indent_size: f32,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct ProjectPanelSettingsContent {
-    pub git_status: Option<bool>,
-    pub dock: Option<ProjectPanelDockPosition>,
     pub default_width: Option<f32>,
+    pub dock: Option<ProjectPanelDockPosition>,
+    pub file_icons: Option<bool>,
+    pub folder_icons: Option<bool>,
+    pub git_status: Option<bool>,
+    pub indent_size: Option<f32>,
 }
 
 impl Setting for ProjectPanelSettings {

crates/project_symbols/src/project_symbols.rs 🔗

@@ -104,7 +104,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
         "Search project symbols...".into()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<ProjectSymbols>) {
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<ProjectSymbols>) {
         if let Some(symbol) = self
             .matches
             .get(self.selected_match_index)
@@ -122,7 +122,12 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                         .read(cx)
                         .clip_point_utf16(symbol.range.start, Bias::Left);
 
-                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
+                    let editor = if secondary {
+                        workspace.split_project_item::<Editor>(buffer, cx)
+                    } else {
+                        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])

crates/recent_projects/src/recent_projects.rs 🔗

@@ -161,7 +161,7 @@ impl PickerDelegate for RecentProjectsDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<RecentProjects>) {
         if let Some((selected_match, workspace)) = self
             .matches
             .get(self.selected_index())

crates/search/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/search.rs"
 doctest = false
 
 [dependencies]
+bitflags = "1"
 collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }

crates/search/src/buffer_search.rs 🔗

@@ -1,15 +1,17 @@
 use crate::{
-    SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
-    ToggleWholeWord,
+    SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
+    ToggleRegex, ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
+use futures::channel::oneshot;
 use gpui::{
     actions,
     elements::*,
     impl_actions,
     platform::{CursorStyle, MouseButton},
     Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
+    WindowContext,
 };
 use project::search::SearchQuery;
 use serde::Deserialize;
@@ -39,23 +41,24 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::focus_editor);
     cx.add_action(BufferSearchBar::select_next_match);
     cx.add_action(BufferSearchBar::select_prev_match);
+    cx.add_action(BufferSearchBar::select_all_matches);
     cx.add_action(BufferSearchBar::select_next_match_on_pane);
     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);
-    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
-    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
-    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
+    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);
 }
 
-fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
+fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
     cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
-                search_bar.update(cx, |search_bar, cx| {
+            search_bar.update(cx, |search_bar, cx| {
+                if search_bar.show(cx) {
                     search_bar.toggle_search_option(option, cx);
-                });
-                return;
-            }
+                }
+            });
         }
         cx.propagate_action();
     });
@@ -66,12 +69,11 @@ pub struct BufferSearchBar {
     active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
     active_match_index: Option<usize>,
     active_searchable_item_subscription: Option<Subscription>,
-    seachable_items_with_matches:
+    searchable_items_with_matches:
         HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
     pending_search: Option<Task<()>>,
-    case_sensitive: bool,
-    whole_word: bool,
-    regex: bool,
+    search_options: SearchOptions,
+    default_options: SearchOptions,
     query_contains_error: bool,
     dismissed: bool,
 }
@@ -118,7 +120,7 @@ impl View for BufferSearchBar {
                             .with_children(self.active_searchable_item.as_ref().and_then(
                                 |searchable_item| {
                                     let matches = self
-                                        .seachable_items_with_matches
+                                        .searchable_items_with_matches
                                         .get(&searchable_item.downgrade())?;
                                     let message = if let Some(match_ix) = self.active_match_index {
                                         format!("{}/{}", match_ix + 1, matches.len())
@@ -146,6 +148,7 @@ impl View for BufferSearchBar {
                         Flex::row()
                             .with_child(self.render_nav_button("<", Direction::Prev, cx))
                             .with_child(self.render_nav_button(">", Direction::Next, cx))
+                            .with_child(self.render_action_button("Select All", cx))
                             .aligned(),
                     )
                     .with_child(
@@ -153,19 +156,19 @@ impl View for BufferSearchBar {
                             .with_children(self.render_search_option(
                                 supported_options.case,
                                 "Case",
-                                SearchOption::CaseSensitive,
+                                SearchOptions::CASE_SENSITIVE,
                                 cx,
                             ))
                             .with_children(self.render_search_option(
                                 supported_options.word,
                                 "Word",
-                                SearchOption::WholeWord,
+                                SearchOptions::WHOLE_WORD,
                                 cx,
                             ))
                             .with_children(self.render_search_option(
                                 supported_options.regex,
                                 "Regex",
-                                SearchOption::Regex,
+                                SearchOptions::REGEX,
                                 cx,
                             ))
                             .contained()
@@ -209,7 +212,7 @@ impl ToolbarItemView for BufferSearchBar {
                 ));
 
             self.active_searchable_item = Some(searchable_item_handle);
-            self.update_matches(false, cx);
+            let _ = self.update_matches(cx);
             if !self.dismissed {
                 return ToolbarItemLocation::Secondary;
             }
@@ -249,10 +252,9 @@ impl BufferSearchBar {
             active_searchable_item: None,
             active_searchable_item_subscription: None,
             active_match_index: None,
-            seachable_items_with_matches: Default::default(),
-            case_sensitive: false,
-            whole_word: false,
-            regex: false,
+            searchable_items_with_matches: Default::default(),
+            default_options: SearchOptions::NONE,
+            search_options: SearchOptions::NONE,
             pending_search: None,
             query_contains_error: false,
             dismissed: true,
@@ -265,7 +267,7 @@ impl BufferSearchBar {
 
     pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
         self.dismissed = true;
-        for searchable_item in self.seachable_items_with_matches.keys() {
+        for searchable_item in self.searchable_items_with_matches.keys() {
             if let Some(searchable_item) =
                 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
             {
@@ -279,48 +281,86 @@ impl BufferSearchBar {
         cx.notify();
     }
 
-    pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
-        let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
-            SearchableItemHandle::boxed_clone(searchable_item.as_ref())
-        } else {
+    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if self.active_searchable_item.is_none() {
             return false;
-        };
-
-        if suggest_query {
-            let text = searchable_item.query_suggestion(cx);
-            if !text.is_empty() {
-                self.set_query(&text, cx);
-            }
-        }
-
-        if focus {
-            let query_editor = self.query_editor.clone();
-            query_editor.update(cx, |query_editor, cx| {
-                query_editor.select_all(&editor::SelectAll, cx);
-            });
-            cx.focus_self();
         }
-
         self.dismissed = false;
         cx.notify();
         cx.emit(Event::UpdateLocation);
         true
     }
 
-    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
+    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
+        let search = self
+            .query_suggestion(cx)
+            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
+
+        if let Some(search) = search {
+            cx.spawn(|this, mut cx| async move {
+                search.await?;
+                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(match_ix) = self.active_match_index {
+            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(matches) = self
+                    .searchable_items_with_matches
+                    .get(&active_searchable_item.downgrade())
+                {
+                    active_searchable_item.activate_match(match_ix, matches, cx)
+                }
+            }
+        }
+    }
+
+    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
-            query_editor.buffer().update(cx, |query_buffer, cx| {
-                let len = query_buffer.len(cx);
-                query_buffer.edit([(0..len, query)], None, cx);
-            });
+            query_editor.select_all(&Default::default(), cx);
         });
     }
 
+    pub fn query(&self, cx: &WindowContext) -> String {
+        self.query_editor.read(cx).text(cx)
+    }
+
+    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
+        self.active_searchable_item
+            .as_ref()
+            .map(|searchable_item| searchable_item.query_suggestion(cx))
+    }
+
+    pub fn search(
+        &mut self,
+        query: &str,
+        options: Option<SearchOptions>,
+        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 {
+            self.query_editor.update(cx, |query_editor, cx| {
+                query_editor.buffer().update(cx, |query_buffer, cx| {
+                    let len = query_buffer.len(cx);
+                    query_buffer.edit([(0..len, query)], None, cx);
+                });
+            });
+            self.search_options = options;
+            self.query_contains_error = false;
+            self.clear_matches(cx);
+            cx.notify();
+        }
+        self.update_matches(cx)
+    }
+
     fn render_search_option(
         &self,
         option_supported: bool,
         icon: &'static str,
-        option: SearchOption,
+        option: SearchOptions,
         cx: &mut ViewContext<Self>,
     ) -> Option<AnyElement<Self>> {
         if !option_supported {
@@ -328,9 +368,9 @@ impl BufferSearchBar {
         }
 
         let tooltip_style = theme::current(cx).tooltip.clone();
-        let is_active = self.is_search_option_enabled(option);
+        let is_active = self.search_options.contains(option);
         Some(
-            MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
+            MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
                 let theme = theme::current(cx);
                 let style = theme
                     .search
@@ -346,7 +386,7 @@ impl BufferSearchBar {
             })
             .with_cursor_style(CursorStyle::PointingHand)
             .with_tooltip::<Self>(
-                option as usize,
+                option.bits as usize,
                 format!("Toggle {}", option.label()),
                 Some(option.to_toggle_action()),
                 tooltip_style,
@@ -401,6 +441,37 @@ impl BufferSearchBar {
         .into_any()
     }
 
+    fn render_action_button(
+        &self,
+        icon: &'static str,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let tooltip = "Select All Matches";
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        let action_type_id = 0_usize;
+
+        enum ActionButton {}
+        MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
+            let theme = theme::current(cx);
+            let style = theme.search.action_button.style_for(state);
+            Label::new(icon, style.text.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.select_all_matches(&SelectAllMatches, cx)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<ActionButton>(
+            action_type_id,
+            tooltip.to_string(),
+            Some(Box::new(SelectAllMatches)),
+            tooltip_style,
+            cx,
+        )
+        .into_any()
+    }
+
     fn render_close_button(
         &self,
         theme: &theme::Search,
@@ -437,12 +508,23 @@ impl BufferSearchBar {
     }
 
     fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+        let mut propagate_action = true;
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
-                return;
-            }
+            search_bar.update(cx, |search_bar, cx| {
+                if search_bar.show(cx) {
+                    search_bar.search_suggested(cx);
+                    if action.focus {
+                        search_bar.select_query(cx);
+                        cx.focus_self();
+                    }
+                    propagate_action = false;
+                }
+            });
+        }
+
+        if propagate_action {
+            cx.propagate_action();
         }
-        cx.propagate_action();
     }
 
     fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
@@ -455,48 +537,59 @@ impl BufferSearchBar {
         cx.propagate_action();
     }
 
-    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
+    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
             cx.focus(active_editor.as_any());
         }
     }
 
-    fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
-        match search_option {
-            SearchOption::WholeWord => self.whole_word,
-            SearchOption::CaseSensitive => self.case_sensitive,
-            SearchOption::Regex => self.regex,
-        }
+    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
+        self.search_options.toggle(search_option);
+        self.default_options = self.search_options;
+        let _ = self.update_matches(cx);
+        cx.notify();
     }
 
-    fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
-        let value = match search_option {
-            SearchOption::WholeWord => &mut self.whole_word,
-            SearchOption::CaseSensitive => &mut self.case_sensitive,
-            SearchOption::Regex => &mut self.regex,
-        };
-        *value = !*value;
-        self.update_matches(false, cx);
+    pub fn set_search_options(
+        &mut self,
+        search_options: SearchOptions,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.search_options = search_options;
         cx.notify();
     }
 
     fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
-        self.select_match(Direction::Next, cx);
+        self.select_match(Direction::Next, 1, cx);
     }
 
     fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
-        self.select_match(Direction::Prev, cx);
+        self.select_match(Direction::Prev, 1, cx);
+    }
+
+    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
+        if !self.dismissed && self.active_match_index.is_some() {
+            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(matches) = self
+                    .searchable_items_with_matches
+                    .get(&searchable_item.downgrade())
+                {
+                    searchable_item.select_matches(matches, cx);
+                    self.focus_editor(&FocusEditor, cx);
+                }
+            }
+        }
     }
 
-    pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
             if let Some(searchable_item) = self.active_searchable_item.as_ref() {
                 if let Some(matches) = self
-                    .seachable_items_with_matches
+                    .searchable_items_with_matches
                     .get(&searchable_item.downgrade())
                 {
-                    let new_match_index =
-                        searchable_item.match_index_for_direction(matches, index, direction, cx);
+                    let new_match_index = searchable_item
+                        .match_index_for_direction(matches, index, direction, count, cx);
                     searchable_item.update_matches(matches, cx);
                     searchable_item.activate_match(new_match_index, matches, cx);
                 }
@@ -524,30 +617,46 @@ impl BufferSearchBar {
         }
     }
 
+    fn select_all_matches_on_pane(
+        pane: &mut Pane,
+        action: &SelectAllMatches,
+        cx: &mut ViewContext<Pane>,
+    ) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
+        }
+    }
+
     fn on_query_editor_event(
         &mut self,
         _: ViewHandle<Editor>,
         event: &editor::Event,
         cx: &mut ViewContext<Self>,
     ) {
-        if let editor::Event::BufferEdited { .. } = event {
+        if let editor::Event::Edited { .. } = event {
             self.query_contains_error = false;
             self.clear_matches(cx);
-            self.update_matches(true, cx);
-            cx.notify();
+            let search = self.update_matches(cx);
+            cx.spawn(|this, mut cx| async move {
+                search.await?;
+                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+            })
+            .detach_and_log_err(cx);
         }
     }
 
     fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
         match event {
-            SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
+            SearchEvent::MatchesInvalidated => {
+                let _ = self.update_matches(cx);
+            }
             SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
         }
     }
 
     fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
         let mut active_item_matches = None;
-        for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
+        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
             if let Some(searchable_item) =
                 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
             {
@@ -559,23 +668,25 @@ impl BufferSearchBar {
             }
         }
 
-        self.seachable_items_with_matches
+        self.searchable_items_with_matches
             .extend(active_item_matches);
     }
 
-    fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
+    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);
         self.pending_search.take();
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
                 active_searchable_item.clear_matches(cx);
+                let _ = done_tx.send(());
             } else {
-                let query = if self.regex {
+                let query = if self.search_options.contains(SearchOptions::REGEX) {
                     match SearchQuery::regex(
                         query,
-                        self.whole_word,
-                        self.case_sensitive,
+                        self.search_options.contains(SearchOptions::WHOLE_WORD),
+                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                         Vec::new(),
                         Vec::new(),
                     ) {
@@ -583,14 +694,14 @@ impl BufferSearchBar {
                         Err(_) => {
                             self.query_contains_error = true;
                             cx.notify();
-                            return;
+                            return done_rx;
                         }
                     }
                 } else {
                     SearchQuery::text(
                         query,
-                        self.whole_word,
-                        self.case_sensitive,
+                        self.search_options.contains(SearchOptions::WHOLE_WORD),
+                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                         Vec::new(),
                         Vec::new(),
                     )
@@ -605,22 +716,17 @@ impl BufferSearchBar {
                         if let Some(active_searchable_item) =
                             WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
                         {
-                            this.seachable_items_with_matches
+                            this.searchable_items_with_matches
                                 .insert(active_searchable_item.downgrade(), matches);
 
                             this.update_match_index(cx);
                             if !this.dismissed {
                                 let matches = this
-                                    .seachable_items_with_matches
+                                    .searchable_items_with_matches
                                     .get(&active_searchable_item.downgrade())
                                     .unwrap();
                                 active_searchable_item.update_matches(matches, cx);
-                                if select_closest_match {
-                                    if let Some(match_ix) = this.active_match_index {
-                                        active_searchable_item
-                                            .activate_match(match_ix, matches, cx);
-                                    }
-                                }
+                                let _ = done_tx.send(());
                             }
                             cx.notify();
                         }
@@ -629,6 +735,7 @@ impl BufferSearchBar {
                 }));
             }
         }
+        done_rx
     }
 
     fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
@@ -637,7 +744,7 @@ impl BufferSearchBar {
             .as_ref()
             .and_then(|searchable_item| {
                 let matches = self
-                    .seachable_items_with_matches
+                    .searchable_items_with_matches
                     .get(&searchable_item.downgrade())?;
                 searchable_item.active_match_index(matches, cx)
             });
@@ -656,8 +763,7 @@ mod tests {
     use language::Buffer;
     use unindent::Unindent as _;
 
-    #[gpui::test]
-    async fn test_search_simple(cx: &mut TestAppContext) {
+    fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
         crate::project_search::tests::init_test(cx);
 
         let buffer = cx.add_model(|cx| {
@@ -680,16 +786,23 @@ mod tests {
         let search_bar = cx.add_view(window_id, |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
-            search_bar.show(false, true, cx);
+            search_bar.show(cx);
             search_bar
         });
 
+        (editor, search_bar)
+    }
+
+    #[gpui::test]
+    async fn test_search_simple(cx: &mut TestAppContext) {
+        let (editor, search_bar) = init_test(cx);
+
         // Search for a string that appears with different casing.
         // By default, search is case-insensitive.
-        search_bar.update(cx, |search_bar, cx| {
-            search_bar.set_query("us", cx);
-        });
-        editor.next_notification(cx).await;
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
+            .await
+            .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
                 editor.all_background_highlights(cx),
@@ -708,7 +821,7 @@ mod tests {
 
         // Switch to a case sensitive search.
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
+            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
         });
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
@@ -723,10 +836,10 @@ mod tests {
 
         // Search for a string that appears both as a whole word and
         // within other words. By default, all results are found.
-        search_bar.update(cx, |search_bar, cx| {
-            search_bar.set_query("or", cx);
-        });
-        editor.next_notification(cx).await;
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
+            .await
+            .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
                 editor.all_background_highlights(cx),
@@ -765,7 +878,7 @@ mod tests {
 
         // Switch to a whole word search.
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.toggle_search_option(SearchOption::WholeWord, cx);
+            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
         });
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
@@ -966,4 +1079,258 @@ mod tests {
             assert_eq!(search_bar.active_match_index, Some(2));
         });
     }
+
+    #[gpui::test]
+    async fn test_search_option_handling(cx: &mut TestAppContext) {
+        let (editor, search_bar) = init_test(cx);
+
+        // show with options should make current search case sensitive
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.show(cx);
+                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
+            })
+            .await
+            .unwrap();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                editor.all_background_highlights(cx),
+                &[(
+                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
+                    Color::red(),
+                )]
+            );
+        });
+
+        // search_suggested should restore default options
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.search_suggested(cx);
+            assert_eq!(search_bar.search_options, SearchOptions::NONE)
+        });
+
+        // toggling a search option should update the defaults
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
+        });
+        editor.next_notification(cx).await;
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                editor.all_background_highlights(cx),
+                &[(
+                    DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
+                    Color::red(),
+                ),]
+            );
+        });
+
+        // defaults should still include whole word
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.search_suggested(cx);
+            assert_eq!(
+                search_bar.search_options,
+                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
+            )
+        });
+    }
+
+    #[gpui::test]
+    async fn test_search_select_all_matches(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 expected_query_matches_count = buffer_text
+            .chars()
+            .filter(|c| c.to_ascii_lowercase() == 'a')
+            .count();
+        assert!(
+            expected_query_matches_count > 1,
+            "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 search_bar = cx.add_view(window_id, |cx| {
+            let mut search_bar = BufferSearchBar::new(cx);
+            search_bar.set_active_pane_item(Some(&editor), cx);
+            search_bar.show(cx);
+            search_bar
+        });
+
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            cx.focus(search_bar.query_editor.as_any());
+            search_bar.activate_current_match(cx);
+        });
+
+        cx.read_window(window_id, |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!(
+                initial_selections.len(), 1,
+                "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
+            );
+            initial_selections
+        });
+        search_bar.update(cx, |search_bar, _| {
+            assert_eq!(search_bar.active_match_index, Some(0));
+        });
+
+        search_bar.update(cx, |search_bar, cx| {
+            cx.focus(search_bar.query_editor.as_any());
+            search_bar.select_all_matches(&SelectAllMatches, cx);
+        });
+        cx.read_window(window_id, |cx| {
+            assert!(
+                editor.is_focused(cx),
+                "Should focus editor after successful SelectAllMatches"
+            );
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            let all_selections =
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+            assert_eq!(
+                all_selections.len(),
+                expected_query_matches_count,
+                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+            );
+            assert_eq!(
+                search_bar.active_match_index,
+                Some(0),
+                "Match index should not change after selecting all matches"
+            );
+        });
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.select_next_match(&SelectNextMatch, cx);
+        });
+        cx.read_window(window_id, |cx| {
+            assert!(
+                editor.is_focused(cx),
+                "Should still have editor focused after SelectNextMatch"
+            );
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            let all_selections =
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+            assert_eq!(
+                all_selections.len(),
+                1,
+                "On next match, should deselect items and select the next match"
+            );
+            assert_ne!(
+                all_selections, initial_selections,
+                "Next match should be different from the first selection"
+            );
+            assert_eq!(
+                search_bar.active_match_index,
+                Some(1),
+                "Match index should be updated to the next one"
+            );
+        });
+
+        search_bar.update(cx, |search_bar, cx| {
+            cx.focus(search_bar.query_editor.as_any());
+            search_bar.select_all_matches(&SelectAllMatches, cx);
+        });
+        cx.read_window(window_id, |cx| {
+            assert!(
+                editor.is_focused(cx),
+                "Should focus editor after successful SelectAllMatches"
+            );
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            let all_selections =
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+            assert_eq!(
+                all_selections.len(),
+                expected_query_matches_count,
+                "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+            );
+            assert_eq!(
+                search_bar.active_match_index,
+                Some(1),
+                "Match index should not change after selecting all matches"
+            );
+        });
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
+        });
+        cx.read_window(window_id, |cx| {
+            assert!(
+                editor.is_focused(cx),
+                "Should still have editor focused after SelectPrevMatch"
+            );
+        });
+        let last_match_selections = search_bar.update(cx, |search_bar, cx| {
+            let all_selections =
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+            assert_eq!(
+                all_selections.len(),
+                1,
+                "On previous match, should deselect items and select the previous item"
+            );
+            assert_eq!(
+                all_selections, initial_selections,
+                "Previous match should be the same as the first selection"
+            );
+            assert_eq!(
+                search_bar.active_match_index,
+                Some(0),
+                "Match index should be updated to the previous one"
+            );
+            all_selections
+        });
+
+        search_bar
+            .update(cx, |search_bar, cx| {
+                cx.focus(search_bar.query_editor.as_any());
+                search_bar.search("abas_nonexistent_match", None, cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.select_all_matches(&SelectAllMatches, cx);
+        });
+        cx.read_window(window_id, |cx| {
+            assert!(
+                !editor.is_focused(cx),
+                "Should not switch focus to editor if SelectAllMatches does not find any matches"
+            );
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            let all_selections =
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+            assert_eq!(
+                all_selections, last_match_selections,
+                "Should not select anything new if there are no matches"
+            );
+            assert!(
+                search_bar.active_match_index.is_none(),
+                "For no matches, there should be no active match index"
+            );
+        });
+    }
 }

crates/search/src/project_search.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
+    SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
     ToggleWholeWord,
 };
 use anyhow::Result;
@@ -18,7 +18,7 @@ use gpui::{
     Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use menu::Confirm;
-use project::{search::SearchQuery, Project};
+use project::{search::SearchQuery, Entry, Project};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -51,12 +51,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::select_prev_match);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
-    add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
-    add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
-    add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
+    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);
 }
 
-fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
+fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
     cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
             if search_bar.update(cx, |search_bar, cx| {
@@ -89,9 +89,7 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
-    case_sensitive: bool,
-    whole_word: bool,
-    regex: bool,
+    search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
     search_id: usize,
@@ -408,9 +406,7 @@ impl ProjectSearchView {
         let project;
         let excerpts;
         let mut query_text = String::new();
-        let mut regex = false;
-        let mut case_sensitive = false;
-        let mut whole_word = false;
+        let mut options = SearchOptions::NONE;
 
         {
             let model = model.read(cx);
@@ -418,9 +414,7 @@ impl ProjectSearchView {
             excerpts = model.excerpts.clone();
             if let Some(active_query) = model.active_query.as_ref() {
                 query_text = active_query.as_str().to_string();
-                regex = active_query.is_regex();
-                case_sensitive = active_query.case_sensitive();
-                whole_word = active_query.whole_word();
+                options = SearchOptions::from_query(active_query);
             }
         }
         cx.observe(&model, |this, _, cx| this.model_changed(cx))
@@ -496,9 +490,7 @@ impl ProjectSearchView {
             model,
             query_editor,
             results_editor,
-            case_sensitive,
-            whole_word,
-            regex,
+            search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
             query_editor_was_focused: false,
@@ -509,6 +501,28 @@ impl ProjectSearchView {
         this
     }
 
+    pub fn new_search_in_directory(
+        workspace: &mut Workspace,
+        dir_entry: &Entry,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        if !dir_entry.is_dir() {
+            return;
+        }
+        let filter_path = dir_entry.path.join("**");
+        let Some(filter_str) = filter_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));
+        workspace.add_item(Box::new(search.clone()), cx);
+        search.update(cx, |search, cx| {
+            search
+                .included_files_editor
+                .update(cx, |editor, cx| editor.set_text(filter_str, cx));
+            search.focus_query_editor(cx)
+        });
+    }
+
     // Re-activate the most recently activated search or the most recent if it has been closed.
     // If no search exists in the workspace, create a new one.
     fn deploy(
@@ -594,11 +608,11 @@ impl ProjectSearchView {
                     return None;
                 }
             };
-        if self.regex {
+        if self.search_options.contains(SearchOptions::REGEX) {
             match SearchQuery::regex(
                 text,
-                self.whole_word,
-                self.case_sensitive,
+                self.search_options.contains(SearchOptions::WHOLE_WORD),
+                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                 included_files,
                 excluded_files,
             ) {
@@ -615,8 +629,8 @@ impl ProjectSearchView {
         } else {
             Some(SearchQuery::text(
                 text,
-                self.whole_word,
-                self.case_sensitive,
+                self.search_options.contains(SearchOptions::WHOLE_WORD),
+                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                 included_files,
                 excluded_files,
             ))
@@ -635,7 +649,7 @@ impl ProjectSearchView {
         if let Some(index) = self.active_match_index {
             let match_ranges = self.model.read(cx).match_ranges.clone();
             let new_index = self.results_editor.update(cx, |editor, cx| {
-                editor.match_index_for_direction(&match_ranges, index, direction, cx)
+                editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
             });
 
             let range_to_select = match_ranges[new_index].clone();
@@ -676,7 +690,6 @@ impl ProjectSearchView {
             self.active_match_index = None;
         } else {
             self.active_match_index = Some(0);
-            self.select_match(Direction::Next, cx);
             self.update_match_index(cx);
             let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
             let is_new_search = self.search_id != prev_search_id;
@@ -768,9 +781,7 @@ impl ProjectSearchBar {
                         search_view.query_editor.update(cx, |editor, cx| {
                             editor.set_text(old_query.as_str(), cx);
                         });
-                        search_view.regex = old_query.is_regex();
-                        search_view.whole_word = old_query.whole_word();
-                        search_view.case_sensitive = old_query.case_sensitive();
+                        search_view.search_options = SearchOptions::from_query(&old_query);
                     }
                 }
                 new_query
@@ -858,15 +869,10 @@ impl ProjectSearchBar {
         });
     }
 
-    fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
+    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                let value = match option {
-                    SearchOption::WholeWord => &mut search_view.whole_word,
-                    SearchOption::CaseSensitive => &mut search_view.case_sensitive,
-                    SearchOption::Regex => &mut search_view.regex,
-                };
-                *value = !*value;
+                search_view.search_options.toggle(option);
                 search_view.search(cx);
             });
             cx.notify();
@@ -923,12 +929,12 @@ impl ProjectSearchBar {
     fn render_option_button(
         &self,
         icon: &'static str,
-        option: SearchOption,
+        option: SearchOptions,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let tooltip_style = theme::current(cx).tooltip.clone();
         let is_active = self.is_option_enabled(option, cx);
-        MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
+        MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
             let theme = theme::current(cx);
             let style = theme
                 .search
@@ -944,7 +950,7 @@ impl ProjectSearchBar {
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .with_tooltip::<Self>(
-            option as usize,
+            option.bits as usize,
             format!("Toggle {}", option.label()),
             Some(option.to_toggle_action()),
             tooltip_style,
@@ -953,14 +959,9 @@ impl ProjectSearchBar {
         .into_any()
     }
 
-    fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
+    fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
         if let Some(search) = self.active_project_search.as_ref() {
-            let search = search.read(cx);
-            match option {
-                SearchOption::WholeWord => search.whole_word,
-                SearchOption::CaseSensitive => search.case_sensitive,
-                SearchOption::Regex => search.regex,
-            }
+            search.read(cx).search_options.contains(option)
         } else {
             false
         }
@@ -1051,17 +1052,17 @@ impl View for ProjectSearchBar {
                             Flex::row()
                                 .with_child(self.render_option_button(
                                     "Case",
-                                    SearchOption::CaseSensitive,
+                                    SearchOptions::CASE_SENSITIVE,
                                     cx,
                                 ))
                                 .with_child(self.render_option_button(
                                     "Word",
-                                    SearchOption::WholeWord,
+                                    SearchOptions::WHOLE_WORD,
                                     cx,
                                 ))
                                 .with_child(self.render_option_button(
                                     "Regex",
-                                    SearchOption::Regex,
+                                    SearchOptions::REGEX,
                                     cx,
                                 ))
                                 .contained()
@@ -1435,6 +1436,134 @@ pub mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_new_project_search_in_directory(
+        deterministic: Arc<Deterministic>,
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a": {
+                    "one.rs": "const ONE: usize = 1;",
+                    "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+                },
+                "b": {
+                    "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 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 active_item = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .active_pane()
+                .read(cx)
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+        });
+        assert!(
+            active_item.is_none(),
+            "Expected no search panel to be active, but got: {active_item:?}"
+        );
+
+        let one_file_entry = cx.update(|cx| {
+            workspace
+                .read(cx)
+                .project()
+                .read(cx)
+                .entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
+                .expect("no entry for /a/one.rs file")
+        });
+        assert!(one_file_entry.is_file());
+        workspace.update(cx, |workspace, cx| {
+            ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
+        });
+        let active_search_entry = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .active_pane()
+                .read(cx)
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+        });
+        assert!(
+            active_search_entry.is_none(),
+            "Expected no search panel to be active for file entry"
+        );
+
+        let a_dir_entry = cx.update(|cx| {
+            workspace
+                .read(cx)
+                .project()
+                .read(cx)
+                .entry_for_path(&(worktree_id, "a").into(), cx)
+                .expect("no entry for /a/ directory")
+        });
+        assert!(a_dir_entry.is_dir());
+        workspace.update(cx, |workspace, cx| {
+            ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
+        });
+
+        let Some(search_view) = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .active_pane()
+                .read(cx)
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+        }) else {
+            panic!("Search view expected to appear after new search in directory event trigger")
+        };
+        deterministic.run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            assert!(
+                search_view.query_editor.is_focused(cx),
+                "On new search in directory, focus should be moved into query editor"
+            );
+            search_view.excluded_files_editor.update(cx, |editor, cx| {
+                assert!(
+                    editor.display_text(cx).is_empty(),
+                    "New search in directory should not have any excluded files"
+                );
+            });
+            search_view.included_files_editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    editor.display_text(cx),
+                    a_dir_entry.path.join("**").display().to_string(),
+                    "New search in directory should have included dir entry path"
+                );
+            });
+        });
+
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("const", cx));
+            search_view.search(cx);
+        });
+        deterministic.run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(
+                search_view
+                    .results_editor
+                    .update(cx, |editor, cx| editor.display_text(cx)),
+                "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+                "New search in directory should have a filter that matches a certain directory"
+            );
+        });
+    }
+
     pub fn init_test(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
         let fonts = cx.font_cache();

crates/search/src/search.rs 🔗

@@ -1,5 +1,7 @@
+use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
 use gpui::{actions, Action, AppContext};
+use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 
 pub mod buffer_search;
@@ -17,31 +19,45 @@ actions!(
         ToggleCaseSensitive,
         ToggleRegex,
         SelectNextMatch,
-        SelectPrevMatch
+        SelectPrevMatch,
+        SelectAllMatches,
     ]
 );
 
-#[derive(Clone, Copy, PartialEq)]
-pub enum SearchOption {
-    WholeWord,
-    CaseSensitive,
-    Regex,
+bitflags! {
+    #[derive(Default)]
+    pub struct SearchOptions: u8 {
+        const NONE = 0b000;
+        const WHOLE_WORD = 0b001;
+        const CASE_SENSITIVE = 0b010;
+        const REGEX = 0b100;
+    }
 }
 
-impl SearchOption {
+impl SearchOptions {
     pub fn label(&self) -> &'static str {
-        match self {
-            SearchOption::WholeWord => "Match Whole Word",
-            SearchOption::CaseSensitive => "Match Case",
-            SearchOption::Regex => "Use Regular Expression",
+        match *self {
+            SearchOptions::WHOLE_WORD => "Match Whole Word",
+            SearchOptions::CASE_SENSITIVE => "Match Case",
+            SearchOptions::REGEX => "Use Regular Expression",
+            _ => panic!("{:?} is not a named SearchOption", self),
         }
     }
 
     pub fn to_toggle_action(&self) -> Box<dyn Action> {
-        match self {
-            SearchOption::WholeWord => Box::new(ToggleWholeWord),
-            SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive),
-            SearchOption::Regex => Box::new(ToggleRegex),
+        match *self {
+            SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
+            SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
+            SearchOptions::REGEX => Box::new(ToggleRegex),
+            _ => panic!("{:?} is not a named SearchOption", self),
         }
     }
+
+    pub fn from_query(query: &SearchQuery) -> SearchOptions {
+        let mut options = SearchOptions::NONE;
+        options.set(SearchOptions::WHOLE_WORD, query.whole_word());
+        options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
+        options.set(SearchOptions::REGEX, query.is_regex());
+        options
+    }
 }

crates/terminal/src/terminal.rs 🔗

@@ -51,7 +51,7 @@ use gpui::{
     fonts,
     geometry::vector::{vec2f, Vector2F},
     keymap_matcher::Keystroke,
-    platform::{MouseButton, MouseMovedEvent, TouchPhase},
+    platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
     scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
     AppContext, ClipboardItem, Entity, ModelContext, Task,
 };
@@ -72,14 +72,17 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
 const DEBUG_CELL_WIDTH: f32 = 5.;
 const DEBUG_LINE_HEIGHT: f32 = 5.;
 
-// Regex Copied from alacritty's ui_config.rs
-
 lazy_static! {
-    static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
+    // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
+    // * avoid Rust-specific escaping.
+    // * 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();
 }
 
 ///Upward flowing events, for changing the title and such
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
 pub enum Event {
     TitleChanged,
     BreadcrumbsChanged,
@@ -88,6 +91,18 @@ pub enum Event {
     Wakeup,
     BlinkChanged,
     SelectionsChanged,
+    NewNavigationTarget(Option<MaybeNavigationTarget>),
+    Open(MaybeNavigationTarget),
+}
+
+/// A string inside terminal, potentially useful as a URI that can be opened.
+#[derive(Clone, Debug)]
+pub enum MaybeNavigationTarget {
+    /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
+    Url(String),
+    /// File system path, absolute or relative, existing or not.
+    /// Might have line and column number(s) attached as `file.rs:1:23`
+    PathLike(String),
 }
 
 #[derive(Clone)]
@@ -493,6 +508,8 @@ impl TerminalBuilder {
             last_mouse_position: None,
             next_link_id: 0,
             selection_phase: SelectionPhase::Ended,
+            cmd_pressed: false,
+            hovered_word: false,
         };
 
         Ok(TerminalBuilder {
@@ -589,7 +606,14 @@ pub struct TerminalContent {
     pub cursor: RenderableCursor,
     pub cursor_char: char,
     pub size: TerminalSize,
-    pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+    pub last_hovered_word: Option<HoveredWord>,
+}
+
+#[derive(Clone)]
+pub struct HoveredWord {
+    pub word: String,
+    pub word_match: RangeInclusive<Point>,
+    pub id: usize,
 }
 
 impl Default for TerminalContent {
@@ -606,7 +630,7 @@ impl Default for TerminalContent {
             },
             cursor_char: Default::default(),
             size: Default::default(),
-            last_hovered_hyperlink: None,
+            last_hovered_word: None,
         }
     }
 }
@@ -623,7 +647,7 @@ pub struct Terminal {
     events: VecDeque<InternalEvent>,
     /// This is only used for mouse mode cell change detection
     last_mouse: Option<(Point, AlacDirection)>,
-    /// This is only used for terminal hyperlink checking
+    /// This is only used for terminal hovered word checking
     last_mouse_position: Option<Vector2F>,
     pub matches: Vec<RangeInclusive<Point>>,
     pub last_content: TerminalContent,
@@ -637,6 +661,8 @@ pub struct Terminal {
     scroll_px: f32,
     next_link_id: usize,
     selection_phase: SelectionPhase,
+    cmd_pressed: bool,
+    hovered_word: bool,
 }
 
 impl Terminal {
@@ -769,7 +795,7 @@ impl Terminal {
             }
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
-                self.refresh_hyperlink();
+                self.refresh_hovered_word();
             }
             InternalEvent::SetSelection(selection) => {
                 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@@ -804,20 +830,20 @@ impl Terminal {
             }
             InternalEvent::ScrollToPoint(point) => {
                 term.scroll_to_point(*point);
-                self.refresh_hyperlink();
+                self.refresh_hovered_word();
             }
             InternalEvent::FindHyperlink(position, open) => {
-                let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+                let prev_hovered_word = self.last_content.last_hovered_word.take();
 
                 let point = grid_point(
                     *position,
                     self.last_content.size,
                     term.grid().display_offset(),
                 )
-                .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
+                .grid_clamp(term, alacritty_terminal::index::Boundary::Grid);
 
                 let link = term.grid().index(point).hyperlink();
-                let found_url = if link.is_some() {
+                let found_word = if link.is_some() {
                     let mut min_index = point;
                     loop {
                         let new_min_index =
@@ -847,42 +873,80 @@ impl Terminal {
                     let url = link.unwrap().uri().to_owned();
                     let url_match = min_index..=max_index;
 
-                    Some((url, url_match))
-                } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
-                    let url = term.bounds_to_string(*url_match.start(), *url_match.end());
-
-                    Some((url, url_match))
+                    Some((url, true, url_match))
+                } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
+                    let maybe_url_or_path =
+                        term.bounds_to_string(*word_match.start(), *word_match.end());
+                    let is_url = match regex_match_at(term, point, &URL_REGEX) {
+                        Some(url_match) => url_match == word_match,
+                        None => false,
+                    };
+                    Some((maybe_url_or_path, is_url, word_match))
                 } else {
                     None
                 };
 
-                if let Some((url, url_match)) = found_url {
-                    if *open {
-                        cx.platform().open_url(url.as_str());
-                    } else {
-                        self.update_hyperlink(prev_hyperlink, url, url_match);
+                match found_word {
+                    Some((maybe_url_or_path, is_url, url_match)) => {
+                        if *open {
+                            let target = if is_url {
+                                MaybeNavigationTarget::Url(maybe_url_or_path)
+                            } else {
+                                MaybeNavigationTarget::PathLike(maybe_url_or_path)
+                            };
+                            cx.emit(Event::Open(target));
+                        } else {
+                            self.update_selected_word(
+                                prev_hovered_word,
+                                url_match,
+                                maybe_url_or_path,
+                                is_url,
+                                cx,
+                            );
+                        }
+                        self.hovered_word = true;
+                    }
+                    None => {
+                        if self.hovered_word {
+                            cx.emit(Event::NewNavigationTarget(None));
+                        }
+                        self.hovered_word = false;
                     }
                 }
             }
         }
     }
 
-    fn update_hyperlink(
+    fn update_selected_word(
         &mut self,
-        prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
-        url: String,
-        url_match: RangeInclusive<Point>,
+        prev_word: Option<HoveredWord>,
+        word_match: RangeInclusive<Point>,
+        word: String,
+        is_url: bool,
+        cx: &mut ModelContext<Self>,
     ) {
-        if let Some(prev_hyperlink) = prev_hyperlink {
-            if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
-                self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
-            } else {
-                self.last_content.last_hovered_hyperlink =
-                    Some((url, url_match, self.next_link_id()));
+        if let Some(prev_word) = prev_word {
+            if prev_word.word == word && prev_word.word_match == word_match {
+                self.last_content.last_hovered_word = Some(HoveredWord {
+                    word,
+                    word_match,
+                    id: prev_word.id,
+                });
+                return;
             }
-        } else {
-            self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
         }
+
+        self.last_content.last_hovered_word = Some(HoveredWord {
+            word: word.clone(),
+            word_match,
+            id: self.next_link_id(),
+        });
+        let navigation_target = if is_url {
+            MaybeNavigationTarget::Url(word)
+        } else {
+            MaybeNavigationTarget::PathLike(word)
+        };
+        cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
     }
 
     fn next_link_id(&mut self) -> usize {
@@ -908,6 +972,21 @@ impl Terminal {
         }
     }
 
+    pub fn select_matches(&mut self, matches: Vec<RangeInclusive<Point>>) {
+        let matches_to_select = self
+            .matches
+            .iter()
+            .filter(|self_match| matches.contains(self_match))
+            .cloned()
+            .collect::<Vec<_>>();
+        for match_to_select in matches_to_select {
+            self.set_selection(Some((
+                make_selection(&match_to_select),
+                *match_to_select.end(),
+            )));
+        }
+    }
+
     fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
         self.events
             .push_back(InternalEvent::SetSelection(selection));
@@ -949,6 +1028,15 @@ impl Terminal {
         }
     }
 
+    pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
+        let changed = self.cmd_pressed != modifiers.cmd;
+        if !self.cmd_pressed && modifiers.cmd {
+            self.refresh_hovered_word();
+        }
+        self.cmd_pressed = modifiers.cmd;
+        changed
+    }
+
     ///Paste text into the terminal
     pub fn paste(&mut self, text: &str) {
         let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
@@ -1020,7 +1108,7 @@ impl Terminal {
             cursor: content.cursor,
             cursor_char: term.grid()[content.cursor.point].c,
             size: last_content.size,
-            last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
+            last_hovered_word: last_content.last_hovered_word.clone(),
         }
     }
 
@@ -1074,14 +1162,14 @@ impl Terminal {
                     self.pty_tx.notify(bytes);
                 }
             }
-        } else {
-            self.hyperlink_from_position(Some(position));
+        } else if self.cmd_pressed {
+            self.word_from_position(Some(position));
         }
     }
 
-    fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
+    fn word_from_position(&mut self, position: Option<Vector2F>) {
         if self.selection_phase == SelectionPhase::Selecting {
-            self.last_content.last_hovered_hyperlink = None;
+            self.last_content.last_hovered_word = None;
         } else if let Some(position) = position {
             self.events
                 .push_back(InternalEvent::FindHyperlink(position, false));
@@ -1193,7 +1281,7 @@ impl Terminal {
                 let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
                 if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
                     cx.platform().open_url(link.uri());
-                } else {
+                } else if self.cmd_pressed {
                     self.events
                         .push_back(InternalEvent::FindHyperlink(position, true));
                 }
@@ -1240,8 +1328,8 @@ impl Terminal {
         }
     }
 
-    pub fn refresh_hyperlink(&mut self) {
-        self.hyperlink_from_position(self.last_mouse_position);
+    fn refresh_hovered_word(&mut self) {
+        self.word_from_position(self.last_mouse_position);
     }
 
     fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
@@ -1319,6 +1407,10 @@ impl Terminal {
             })
             .unwrap_or_else(|| "Terminal".to_string())
     }
+
+    pub fn can_navigate_to_selected_word(&self) -> bool {
+        self.cmd_pressed && self.hovered_word
+    }
 }
 
 impl Drop for Terminal {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -10,8 +10,9 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     serde_json::json,
     text_layout::{Line, RunStyle},
-    AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion, Quad,
-    SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext, WeakModelHandle,
+    AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion,
+    PaintContext, Quad, SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext,
+    WeakModelHandle,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -163,6 +164,7 @@ pub struct TerminalElement {
     terminal: WeakModelHandle<Terminal>,
     focused: bool,
     cursor_visible: bool,
+    can_navigate_to_selected_word: bool,
 }
 
 impl TerminalElement {
@@ -170,11 +172,13 @@ impl TerminalElement {
         terminal: WeakModelHandle<Terminal>,
         focused: bool,
         cursor_visible: bool,
+        can_navigate_to_selected_word: bool,
     ) -> TerminalElement {
         TerminalElement {
             terminal,
             focused,
             cursor_visible,
+            can_navigate_to_selected_word,
         }
     }
 
@@ -580,20 +584,30 @@ impl Element<TerminalView> for TerminalElement {
         let background_color = terminal_theme.background;
         let terminal_handle = self.terminal.upgrade(cx).unwrap();
 
-        let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| {
+        let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
             terminal.set_size(dimensions);
             terminal.try_sync(cx);
-            terminal.last_content.last_hovered_hyperlink.clone()
+            if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
+                terminal.last_content.last_hovered_word.clone()
+            } else {
+                None
+            }
         });
 
-        let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| {
+        let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
             let mut tooltip = Overlay::new(
                 Empty::new()
                     .contained()
                     .constrained()
                     .with_width(dimensions.width())
                     .with_height(dimensions.height())
-                    .with_tooltip::<TerminalElement>(id, uri, None, tooltip_style, cx),
+                    .with_tooltip::<TerminalElement>(
+                        hovered_word.id,
+                        hovered_word.word,
+                        None,
+                        tooltip_style,
+                        cx,
+                    ),
             )
             .with_position_mode(gpui::elements::OverlayPositionMode::Local)
             .into_any();
@@ -613,7 +627,6 @@ impl Element<TerminalView> for TerminalElement {
             cursor_char,
             selection,
             cursor,
-            last_hovered_hyperlink,
             ..
         } = { &terminal_handle.read(cx).last_content };
 
@@ -634,9 +647,9 @@ impl Element<TerminalView> for TerminalElement {
             &terminal_theme,
             cx.text_layout_cache(),
             cx.font_cache(),
-            last_hovered_hyperlink
+            last_hovered_word
                 .as_ref()
-                .map(|(_, range, _)| (link_style, range)),
+                .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
         );
 
         //Layout cursor. Rectangle is used for IME, so we should lay it out even
@@ -718,7 +731,7 @@ impl Element<TerminalView> for TerminalElement {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut TerminalView,
-        cx: &mut ViewContext<TerminalView>,
+        cx: &mut PaintContext<TerminalView>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -261,10 +261,14 @@ impl TerminalPanel {
                         .create_terminal(working_directory, window_id, cx)
                         .log_err()
                 }) {
-                    let terminal =
-                        Box::new(cx.add_view(|cx| {
-                            TerminalView::new(terminal, workspace.database_id(), cx)
-                        }));
+                    let terminal = Box::new(cx.add_view(|cx| {
+                        TerminalView::new(
+                            terminal,
+                            workspace.weak_handle(),
+                            workspace.database_id(),
+                            cx,
+                        )
+                    }));
                     pane.update(cx, |pane, cx| {
                         let focus = pane.has_focus();
                         pane.add_item(terminal, true, focus, None, cx);

crates/terminal_view/src/terminal_view.rs 🔗

@@ -3,18 +3,21 @@ pub mod terminal_element;
 pub mod terminal_panel;
 
 use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
+use anyhow::Context;
 use context_menu::{ContextMenu, ContextMenuItem};
 use dirs::home_dir;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
     actions,
     elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
     geometry::vector::Vector2F,
     impl_actions,
     keymap_matcher::{KeymapContext, Keystroke},
-    platform::KeyDownEvent,
+    platform::{KeyDownEvent, ModifiersChangedEvent},
     AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
+use language::Bias;
 use project::{LocalWorktree, Project};
 use serde::Deserialize;
 use smallvec::{smallvec, SmallVec};
@@ -30,9 +33,9 @@ use terminal::{
         index::Point,
         term::{search::RegexSearch, TermMode},
     },
-    Event, Terminal, TerminalBlink, WorkingDirectory,
+    Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory,
 };
-use util::ResultExt;
+use util::{paths::PathLikeWithPosition, ResultExt};
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent},
     notifications::NotifyResultExt,
@@ -90,6 +93,7 @@ pub struct TerminalView {
     blinking_on: bool,
     blinking_paused: bool,
     blink_epoch: usize,
+    can_navigate_to_selected_word: bool,
     workspace_id: WorkspaceId,
 }
 
@@ -117,19 +121,27 @@ impl TerminalView {
             .notify_err(workspace, cx);
 
         if let Some(terminal) = terminal {
-            let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+            let view = cx.add_view(|cx| {
+                TerminalView::new(
+                    terminal,
+                    workspace.weak_handle(),
+                    workspace.database_id(),
+                    cx,
+                )
+            });
             workspace.add_item(Box::new(view), cx)
         }
     }
 
     pub fn new(
         terminal: ModelHandle<Terminal>,
+        workspace: WeakViewHandle<Workspace>,
         workspace_id: WorkspaceId,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let view_id = cx.view_id();
         cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
-        cx.subscribe(&terminal, |this, _, event, cx| match event {
+        cx.subscribe(&terminal, move |this, _, event, cx| match event {
             Event::Wakeup => {
                 if !cx.is_self_focused() {
                     this.has_new_content = true;
@@ -158,7 +170,82 @@ impl TerminalView {
                         .detach();
                 }
             }
-            _ => cx.emit(*event),
+            Event::NewNavigationTarget(maybe_navigation_target) => {
+                this.can_navigate_to_selected_word = match maybe_navigation_target {
+                    Some(MaybeNavigationTarget::Url(_)) => true,
+                    Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
+                        !possible_open_targets(&workspace, maybe_path, cx).is_empty()
+                    }
+                    None => false,
+                }
+            }
+            Event::Open(maybe_navigation_target) => match maybe_navigation_target {
+                MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
+                MaybeNavigationTarget::PathLike(maybe_path) => {
+                    if !this.can_navigate_to_selected_word {
+                        return;
+                    }
+                    let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
+                    if let Some(path) = potential_abs_paths.into_iter().next() {
+                        let is_dir = path.path_like.is_dir();
+                        let task_workspace = workspace.clone();
+                        cx.spawn(|_, mut cx| async move {
+                            let opened_items = task_workspace
+                                .update(&mut cx, |workspace, cx| {
+                                    workspace.open_paths(vec![path.path_like], is_dir, cx)
+                                })
+                                .context("workspace update")?
+                                .await;
+                            anyhow::ensure!(
+                                opened_items.len() == 1,
+                                "For a single path open, expected single opened item"
+                            );
+                            let opened_item = opened_items
+                                .into_iter()
+                                .next()
+                                .unwrap()
+                                .transpose()
+                                .context("path open")?;
+                            if is_dir {
+                                task_workspace.update(&mut cx, |workspace, cx| {
+                                    workspace.project().update(cx, |_, cx| {
+                                        cx.emit(project::Event::ActivateProjectPanel);
+                                    })
+                                })?;
+                            } else {
+                                if let Some(row) = path.row {
+                                    let col = path.column.unwrap_or(0);
+                                    if let Some(active_editor) =
+                                        opened_item.and_then(|item| item.downcast::<Editor>())
+                                    {
+                                        active_editor
+                                            .downgrade()
+                                            .update(&mut cx, |editor, cx| {
+                                                let snapshot = editor.snapshot(cx).display_snapshot;
+                                                let point = snapshot.buffer_snapshot.clip_point(
+                                                    language::Point::new(
+                                                        row.saturating_sub(1),
+                                                        col.saturating_sub(1),
+                                                    ),
+                                                    Bias::Left,
+                                                );
+                                                editor.change_selections(
+                                                    Some(Autoscroll::center()),
+                                                    cx,
+                                                    |s| s.select_ranges([point..point]),
+                                                );
+                                            })
+                                            .log_err();
+                                    }
+                                }
+                            }
+                            anyhow::Ok(())
+                        })
+                        .detach_and_log_err(cx);
+                    }
+                }
+            },
+            _ => cx.emit(event.clone()),
         })
         .detach();
 
@@ -171,6 +258,7 @@ impl TerminalView {
             blinking_on: false,
             blinking_paused: false,
             blink_epoch: 0,
+            can_navigate_to_selected_word: false,
             workspace_id,
         }
     }
@@ -344,6 +432,50 @@ impl TerminalView {
     }
 }
 
+fn possible_open_targets(
+    workspace: &WeakViewHandle<Workspace>,
+    maybe_path: &String,
+    cx: &mut ViewContext<'_, '_, TerminalView>,
+) -> Vec<PathLikeWithPosition<PathBuf>> {
+    let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
+        Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
+    })
+    .expect("infallible");
+    let maybe_path = path_like.path_like;
+    let potential_abs_paths = if maybe_path.is_absolute() {
+        vec![maybe_path]
+    } else if maybe_path.starts_with("~") {
+        if let Some(abs_path) = maybe_path
+            .strip_prefix("~")
+            .ok()
+            .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
+        {
+            vec![abs_path]
+        } else {
+            Vec::new()
+        }
+    } else if let Some(workspace) = workspace.upgrade(cx) {
+        workspace.update(cx, |workspace, cx| {
+            workspace
+                .worktrees(cx)
+                .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
+                .collect()
+        })
+    } else {
+        Vec::new()
+    };
+
+    potential_abs_paths
+        .into_iter()
+        .filter(|path| path.exists())
+        .map(|path| PathLikeWithPosition {
+            path_like: path,
+            row: path_like.row,
+            column: path_like.column,
+        })
+        .collect()
+}
+
 pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
     let searcher = match query {
         project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
@@ -372,6 +504,7 @@ impl View for TerminalView {
                     terminal_handle,
                     focused,
                     self.should_show_cursor(focused, cx),
+                    self.can_navigate_to_selected_word,
                 )
                 .contained(),
             )
@@ -393,6 +526,20 @@ impl View for TerminalView {
         cx.notify();
     }
 
+    fn modifiers_changed(
+        &mut self,
+        event: &ModifiersChangedEvent,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let handled = self
+            .terminal()
+            .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
+        if handled {
+            cx.notify();
+        }
+        handled
+    }
+
     fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
         self.clear_bel(cx);
         self.pause_cursor_blinking(cx);
@@ -618,7 +765,7 @@ impl Item for TerminalView {
                 project.create_terminal(cwd, window_id, cx)
             })?;
             Ok(pane.update(&mut cx, |_, cx| {
-                cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx))
+                cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
             })?)
         })
     }
@@ -647,7 +794,11 @@ impl SearchableItem for TerminalView {
     }
 
     /// Convert events raised by this item into search-relevant events (if applicable)
-    fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        _: &mut ViewContext<Self>,
+    ) -> Option<SearchEvent> {
         match event {
             Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
             Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
@@ -682,6 +833,13 @@ impl SearchableItem for TerminalView {
         cx.notify();
     }
 
+    /// Add selections for all matches given.
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.terminal()
+            .update(cx, |term, _| term.select_matches(matches));
+        cx.notify();
+    }
+
     /// Get all of the matches for this query, should be done on the background
     fn find_matches(
         &mut self,

crates/theme/src/theme.rs 🔗

@@ -350,6 +350,7 @@ pub struct Tab {
     pub icon_close_active: Color,
     pub icon_dirty: Color,
     pub icon_conflict: Color,
+    pub git: GitProjectStatus,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -379,6 +380,7 @@ pub struct Search {
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
     pub option_button: Toggleable<Interactive<ContainedText>>,
+    pub action_button: Interactive<ContainedText>,
     pub match_background: Color,
     pub match_index: ContainedText,
     pub results_status: TextStyle,
@@ -478,8 +480,10 @@ pub struct ProjectPanelEntry {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub text: TextStyle,
-    pub icon_color: Color,
     pub icon_size: f32,
+    pub icon_color: Color,
+    pub chevron_color: Color,
+    pub chevron_size: f32,
     pub icon_spacing: f32,
     pub status: EntryStatus,
 }
@@ -687,6 +691,8 @@ pub struct Editor {
     pub document_highlight_read_background: Color,
     pub document_highlight_write_background: Color,
     pub diff: DiffStyle,
+    pub wrap_guide: Color,
+    pub active_wrap_guide: Color,
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
@@ -721,12 +727,12 @@ pub struct Scrollbar {
     pub thumb: ContainerStyle,
     pub width: f32,
     pub min_height_factor: f32,
-    pub git: GitDiffColors,
+    pub git: BufferGitDiffColors,
     pub selections: Color,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
-pub struct GitDiffColors {
+pub struct BufferGitDiffColors {
     pub inserted: Color,
     pub modified: Color,
     pub deleted: Color,

crates/theme/src/theme_registry.rs 🔗

@@ -5,6 +5,7 @@ use parking_lot::Mutex;
 use serde::Deserialize;
 use serde_json::Value;
 use std::{
+    borrow::Cow,
     collections::HashMap,
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
@@ -43,7 +44,7 @@ impl ThemeRegistry {
         this
     }
 
-    pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
+    pub fn list_names(&self, staff: bool) -> impl Iterator<Item = Cow<str>> + '_ {
         let mut dirs = self.assets.list("themes/");
 
         if !staff {
@@ -53,10 +54,21 @@ impl ThemeRegistry {
                 .collect()
         }
 
-        dirs.into_iter().filter_map(|path| {
-            let filename = path.strip_prefix("themes/")?;
-            let theme_name = filename.strip_suffix(".json")?;
-            self.get(theme_name).ok().map(|theme| theme.meta.clone())
+        fn get_name(path: &str) -> Option<&str> {
+            path.strip_prefix("themes/")?.strip_suffix(".json")
+        }
+
+        dirs.into_iter().filter_map(|path| match path {
+            Cow::Borrowed(path) => Some(Cow::Borrowed(get_name(path)?)),
+            Cow::Owned(path) => Some(Cow::Owned(get_name(&path)?.to_string())),
+        })
+    }
+
+    pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
+        self.list_names(staff).filter_map(|theme_name| {
+            self.get(theme_name.as_ref())
+                .ok()
+                .map(|theme| theme.meta.clone())
         })
     }
 

crates/theme/src/theme_settings.rs 🔗

@@ -178,8 +178,8 @@ impl settings::Setting for ThemeSettings {
         let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
         let theme_names = cx
             .global::<Arc<ThemeRegistry>>()
-            .list(params.staff_mode)
-            .map(|theme| Value::String(theme.name.clone()))
+            .list_names(params.staff_mode)
+            .map(|theme_name| Value::String(theme_name.to_string()))
             .collect();
 
         let theme_name_schema = SchemaObject {

crates/theme_selector/src/theme_selector.rs 🔗

@@ -120,7 +120,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<ThemeSelector>) {
         self.selection_completed = true;
 
         let theme_name = theme::current(cx).meta.name.clone();

crates/vcs_menu/src/lib.rs 🔗

@@ -106,12 +106,14 @@ impl PickerDelegate for BranchListDelegate {
                 .read_with(&mut cx, |view, cx| {
                     let delegate = view.delegate();
                     let project = delegate.workspace.read(cx).project().read(&cx);
-                    let mut cwd =
-                    project
+
+                    let Some(worktree) = project
                         .visible_worktrees(cx)
                         .next()
-                        .unwrap()
-                        .read(cx)
+                    else {
+                        bail!("Cannot update branch list as there are no visible worktrees")
+                    };
+                    let mut cwd = worktree .read(cx)
                         .abs_path()
                         .to_path_buf();
                     cwd.push(".git");
@@ -180,9 +182,11 @@ impl PickerDelegate for BranchListDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         let current_pick = self.selected_index();
-        let current_pick = self.matches[current_pick].string.clone();
+        let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else {
+            return;
+        };
         cx.spawn(|picker, mut cx| async move {
             picker
                 .update(&mut cx, |this, cx| {

crates/vector_store/src/embedding.rs 🔗

@@ -67,11 +67,13 @@ impl EmbeddingProvider for DummyEmbeddings {
     }
 }
 
+const INPUT_LIMIT: usize = 8190;
+
 impl OpenAIEmbeddings {
-    async fn truncate(span: String) -> String {
+    fn truncate(span: String) -> String {
         let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref());
-        if tokens.len() > 8190 {
-            tokens.truncate(8190);
+        if tokens.len() > INPUT_LIMIT {
+            tokens.truncate(INPUT_LIMIT);
             let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
             if result.is_ok() {
                 let transformed = result.unwrap();
@@ -80,7 +82,7 @@ impl OpenAIEmbeddings {
             }
         }
 
-        return span.to_string();
+        span
     }
 
     async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> {
@@ -137,7 +139,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                     // 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()).await;
+                        *span = Self::truncate(span.to_string());
                     }
                 }
                 StatusCode::OK => {

crates/vector_store/src/modal.rs 🔗

@@ -51,7 +51,7 @@ impl PickerDelegate for SemanticSearchDelegate {
         "Search repository in natural language...".into()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
+    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();

crates/vector_store/src/parsing.rs 🔗

@@ -63,7 +63,7 @@ impl CodeContextRetriever {
         ) {
             // log::info!("-----MATCH-----");
 
-            let mut name: Vec<&str> = vec![];
+            let mut name = Vec::new();
             let mut item: Option<&str> = None;
             let mut offset: Option<usize> = None;
             for capture in mat.captures {
@@ -91,11 +91,8 @@ impl CodeContextRetriever {
                     .replace("<language>", &pending_file.language.name().to_lowercase())
                     .replace("<item>", item.unwrap());
 
-                let mut truncated_span = context_span.clone();
-                truncated_span.truncate(100);
-
                 // log::info!("Name:       {:?}", name);
-                // log::info!("Span:       {:?}", truncated_span);
+                // log::info!("Span:       {:?}", util::truncate(&context_span, 100));
 
                 context_spans.push(context_span);
                 documents.push(Document {

crates/vim/src/editor_events.rs 🔗

@@ -13,7 +13,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
         cx.update_window(previously_active_editor.window_id(), |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.update_active_editor(cx, |previously_active_editor, cx| {
-                    Vim::unhook_vim_settings(previously_active_editor, cx);
+                    vim.unhook_vim_settings(previously_active_editor, cx)
                 });
             });
         });
@@ -35,7 +35,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
                 }
             }
 
-            editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
+            editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx))
         });
     });
 }

crates/vim/src/motion.rs 🔗

@@ -62,6 +62,12 @@ struct PreviousWordStart {
     ignore_punctuation: bool,
 }
 
+#[derive(Clone, Deserialize, PartialEq)]
+struct RepeatFind {
+    #[serde(default)]
+    backwards: bool,
+}
+
 actions!(
     vim,
     [
@@ -82,7 +88,10 @@ actions!(
         NextLineStart,
     ]
 );
-impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
+impl_actions!(
+    vim,
+    [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
+);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
@@ -123,13 +132,15 @@ pub fn init(cx: &mut AppContext) {
          &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
-    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
+    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
+    cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
+        repeat_motion(action.backwards, cx)
+    })
 }
 
 pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
-    if let Some(Operator::Namespace(_))
-    | Some(Operator::FindForward { .. })
-    | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
+    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
+        Vim::read(cx).active_operator()
     {
         Vim::update(cx, |vim, cx| vim.pop_operator(cx));
     }
@@ -146,6 +157,35 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 }
 
+fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
+    let find = match Vim::read(cx).state.last_find.clone() {
+        Some(Motion::FindForward { before, text }) => {
+            if backwards {
+                Motion::FindBackward {
+                    after: before,
+                    text,
+                }
+            } else {
+                Motion::FindForward { before, text }
+            }
+        }
+
+        Some(Motion::FindBackward { after, text }) => {
+            if backwards {
+                Motion::FindForward {
+                    before: after,
+                    text,
+                }
+            } else {
+                Motion::FindBackward { after, text }
+            }
+        }
+        _ => return,
+    };
+
+    motion(find, cx)
+}
+
 // Motion handling is specified here:
 // https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 impl Motion {
@@ -743,4 +783,23 @@ mod test {
         cx.simulate_shared_keystrokes(["%"]).await;
         cx.assert_shared_state("func boop(ˇ) {\n}").await;
     }
+
+    #[gpui::test]
+    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("ˇone two three four").await;
+        cx.simulate_shared_keystrokes(["f", "o"]).await;
+        cx.assert_shared_state("one twˇo three four").await;
+        cx.simulate_shared_keystrokes([","]).await;
+        cx.assert_shared_state("ˇone two three four").await;
+        cx.simulate_shared_keystrokes(["2", ";"]).await;
+        cx.assert_shared_state("one two three fˇour").await;
+        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
+        cx.assert_shared_state("one two threeˇ four").await;
+        cx.simulate_shared_keystrokes(["3", ";"]).await;
+        cx.assert_shared_state("oneˇ two three four").await;
+        cx.simulate_shared_keystrokes([","]).await;
+        cx.assert_shared_state("one two thˇree four").await;
+    }
 }

crates/vim/src/normal.rs 🔗

@@ -2,6 +2,7 @@ mod case;
 mod change;
 mod delete;
 mod scroll;
+mod search;
 mod substitute;
 mod yank;
 
@@ -57,6 +58,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(insert_line_above);
     cx.add_action(insert_line_below);
     cx.add_action(change_case);
+    search::init(cx);
     cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);
@@ -105,7 +107,7 @@ pub fn normal_motion(
             Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
             Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
             Some(operator) => {
-                // Can't do anything for text objects or namespace operators. Ignoring
+                // Can't do anything for text objects, Ignoring
                 error!("Unexpected normal mode motion operator: {:?}", operator)
             }
         }
@@ -439,11 +441,8 @@ mod test {
     use indoc::indoc;
 
     use crate::{
-        state::{
-            Mode::{self, *},
-            Namespace, Operator,
-        },
-        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
+        state::Mode::{self},
+        test::{ExemptionFeatures, NeovimBackedTestContext},
     };
 
     #[gpui::test]
@@ -608,22 +607,6 @@ mod test {
             .await;
     }
 
-    #[gpui::test]
-    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
-        let mut cx = VimTestContext::new(cx, true).await;
-
-        // Can abort with escape to get back to normal mode
-        cx.simulate_keystroke("g");
-        assert_eq!(cx.mode(), Normal);
-        assert_eq!(
-            cx.active_operator(),
-            Some(Operator::Namespace(Namespace::G))
-        );
-        cx.simulate_keystroke("escape");
-        assert_eq!(cx.mode(), Normal);
-        assert_eq!(cx.active_operator(), None);
-    }
-
     #[gpui::test]
     async fn test_gg(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

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

@@ -0,0 +1,302 @@
+use gpui::{actions, impl_actions, AppContext, ViewContext};
+use search::{buffer_search, BufferSearchBar, SearchOptions};
+use serde_derive::Deserialize;
+use workspace::{searchable::Direction, Pane, Workspace};
+
+use crate::{state::SearchState, Vim};
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct MoveToNext {
+    #[serde(default)]
+    partial_word: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct MoveToPrev {
+    #[serde(default)]
+    partial_word: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub(crate) struct Search {
+    #[serde(default)]
+    backwards: bool,
+}
+
+impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
+actions!(vim, [SearchSubmit]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+    cx.add_action(move_to_next);
+    cx.add_action(move_to_prev);
+    cx.add_action(search);
+    cx.add_action(search_submit);
+    cx.add_action(search_deploy);
+}
+
+fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
+    move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
+}
+
+fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
+    move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
+}
+
+fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
+    let pane = workspace.active_pane().clone();
+    let direction = if action.backwards {
+        Direction::Prev
+    } else {
+        Direction::Next
+    };
+    Vim::update(cx, |vim, cx| {
+        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        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| {
+                    if !search_bar.show(cx) {
+                        return;
+                    }
+                    let query = search_bar.query(cx);
+
+                    search_bar.select_query(cx);
+                    cx.focus_self();
+
+                    if query.is_empty() {
+                        search_bar.set_search_options(
+                            SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
+                            cx,
+                        );
+                    }
+                    vim.state.search = SearchState {
+                        direction,
+                        count,
+                        initial_query: query,
+                    };
+                });
+            }
+        })
+    })
+}
+
+// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
+fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
+    Vim::update(cx, |vim, _| vim.state.search = Default::default());
+    cx.propagate_action();
+}
+
+fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        let pane = workspace.active_pane().clone();
+        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 mut count = state.count;
+
+                    // in the case that the query has changed, the search bar
+                    // will have selected the next match already.
+                    if (search_bar.query(cx) != state.initial_query)
+                        && state.direction == Direction::Next
+                    {
+                        count = count.saturating_sub(1)
+                    }
+                    search_bar.select_match(state.direction, count, cx);
+                    state.count = 1;
+                    search_bar.focus_editor(&Default::default(), cx);
+                });
+            }
+        });
+    })
+}
+
+pub fn move_to_internal(
+    workspace: &mut Workspace,
+    direction: Direction,
+    whole_word: bool,
+    cx: &mut ViewContext<Workspace>,
+) {
+    Vim::update(cx, |vim, cx| {
+        let pane = workspace.active_pane().clone();
+        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        pane.update(cx, |pane, cx| {
+            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+                let search = search_bar.update(cx, |search_bar, cx| {
+                    let mut options = SearchOptions::CASE_SENSITIVE;
+                    options.set(SearchOptions::WHOLE_WORD, whole_word);
+                    if search_bar.show(cx) {
+                        search_bar
+                            .query_suggestion(cx)
+                            .map(|query| search_bar.search(&query, Some(options), cx))
+                    } else {
+                        None
+                    }
+                });
+
+                if let Some(search) = search {
+                    let search_bar = search_bar.downgrade();
+                    cx.spawn(|_, mut cx| async move {
+                        search.await?;
+                        search_bar.update(&mut cx, |search_bar, cx| {
+                            search_bar.select_match(direction, count, cx)
+                        })?;
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+            }
+        });
+        vim.clear_operator(cx);
+    });
+}
+
+#[cfg(test)]
+mod test {
+    use std::sync::Arc;
+
+    use editor::DisplayPoint;
+    use search::BufferSearchBar;
+
+    use crate::{state::Mode, test::VimTestContext};
+
+    #[gpui::test]
+    async fn test_move_to_next(
+        cx: &mut gpui::TestAppContext,
+        deterministic: Arc<gpui::executor::Deterministic>,
+    ) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["*"]);
+        deterministic.run_until_parked();
+        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["*"]);
+        deterministic.run_until_parked();
+        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["#"]);
+        deterministic.run_until_parked();
+        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["#"]);
+        deterministic.run_until_parked();
+        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["2", "*"]);
+        deterministic.run_until_parked();
+        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["g", "*"]);
+        deterministic.run_until_parked();
+        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["n"]);
+        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
+
+        cx.simulate_keystrokes(["g", "#"]);
+        deterministic.run_until_parked();
+        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
+    }
+
+    #[gpui::test]
+    async fn test_search(
+        cx: &mut gpui::TestAppContext,
+        deterministic: Arc<gpui::executor::Deterministic>,
+    ) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
+        cx.simulate_keystrokes(["/", "c", "c"]);
+
+        let search_bar = cx.workspace(|workspace, cx| {
+            workspace
+                .active_pane()
+                .read(cx)
+                .toolbar()
+                .read(cx)
+                .item_of_type::<BufferSearchBar>()
+                .expect("Buffer search bar should be deployed")
+        });
+
+        search_bar.read_with(cx.cx, |bar, cx| {
+            assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+        });
+
+        deterministic.run_until_parked();
+
+        cx.update_editor(|editor, cx| {
+            let highlights = editor.all_background_highlights(cx);
+            assert_eq!(3, highlights.len());
+            assert_eq!(
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
+                highlights[0].0
+            )
+        });
+
+        cx.simulate_keystrokes(["enter"]);
+        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
+
+        // n to go to next/N to go to previous
+        cx.simulate_keystrokes(["n"]);
+        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
+        cx.simulate_keystrokes(["shift-n"]);
+        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
+
+        // ?<enter> to go to previous
+        cx.simulate_keystrokes(["?", "enter"]);
+        deterministic.run_until_parked();
+        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
+        cx.simulate_keystrokes(["?", "enter"]);
+        deterministic.run_until_parked();
+        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
+
+        // /<enter> to go to next
+        cx.simulate_keystrokes(["/", "enter"]);
+        deterministic.run_until_parked();
+        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
+
+        // ?{search}<enter> to search backwards
+        cx.simulate_keystrokes(["?", "b", "enter"]);
+        deterministic.run_until_parked();
+        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
+
+        // works with counts
+        cx.simulate_keystrokes(["4", "/", "c"]);
+        deterministic.run_until_parked();
+        cx.simulate_keystrokes(["enter"]);
+        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
+
+        // check that searching resumes from cursor, not previous match
+        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
+        cx.simulate_keystrokes(["/", "d"]);
+        deterministic.run_until_parked();
+        cx.simulate_keystrokes(["enter"]);
+        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
+        cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
+        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
+        cx.simulate_keystrokes(["/", "b"]);
+        deterministic.run_until_parked();
+        cx.simulate_keystrokes(["enter"]);
+        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
+    }
+
+    #[gpui::test]
+    async fn test_non_vim_search(
+        cx: &mut gpui::TestAppContext,
+        deterministic: Arc<gpui::executor::Deterministic>,
+    ) {
+        let mut cx = VimTestContext::new(cx, false).await;
+        cx.set_state("ˇone one one one", Mode::Normal);
+        cx.simulate_keystrokes(["cmd-f"]);
+        deterministic.run_until_parked();
+
+        cx.assert_editor_state("«oneˇ» one one one");
+        cx.simulate_keystrokes(["enter"]);
+        cx.assert_editor_state("one «oneˇ» one one");
+        cx.simulate_keystrokes(["shift-enter"]);
+        cx.assert_editor_state("«oneˇ» one one one");
+    }
+}

crates/vim/src/state.rs 🔗

@@ -1,6 +1,9 @@
 use gpui::keymap_matcher::KeymapContext;
 use language::CursorShape;
 use serde::{Deserialize, Serialize};
+use workspace::searchable::Direction;
+
+use crate::motion::Motion;
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
 pub enum Mode {
@@ -15,16 +18,9 @@ impl Default for Mode {
     }
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-pub enum Namespace {
-    G,
-    Z,
-}
-
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
 pub enum Operator {
     Number(usize),
-    Namespace(Namespace),
     Change,
     Delete,
     Yank,
@@ -38,6 +34,25 @@ pub enum Operator {
 pub struct VimState {
     pub mode: Mode,
     pub operator_stack: Vec<Operator>,
+    pub search: SearchState,
+
+    pub last_find: Option<Motion>,
+}
+
+pub struct SearchState {
+    pub direction: Direction,
+    pub count: usize,
+    pub initial_query: String,
+}
+
+impl Default for SearchState {
+    fn default() -> Self {
+        Self {
+            direction: Direction::Next,
+            count: 1,
+            initial_query: "".to_string(),
+        }
+    }
 }
 
 impl VimState {
@@ -73,6 +88,7 @@ impl VimState {
 
     pub fn keymap_context_layer(&self) -> KeymapContext {
         let mut context = KeymapContext::default();
+        context.add_identifier("VimEnabled");
         context.add_key(
             "vim_mode",
             match self.mode {
@@ -107,8 +123,6 @@ impl Operator {
     pub fn id(&self) -> &'static str {
         match self {
             Operator::Number(_) => "n",
-            Operator::Namespace(Namespace::G) => "g",
-            Operator::Namespace(Namespace::Z) => "z",
             Operator::Object { around: false } => "i",
             Operator::Object { around: true } => "a",
             Operator::Change => "c",

crates/vim/src/test.rs 🔗

@@ -5,6 +5,7 @@ mod vim_binding_test_context;
 mod vim_test_context;
 
 use command_palette::CommandPalette;
+use editor::DisplayPoint;
 pub use neovim_backed_binding_test_context::*;
 pub use neovim_backed_test_context::*;
 pub use vim_binding_test_context::*;
@@ -96,7 +97,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), "jumps");
+        assert_eq!(bar.query_editor.read(cx).text(cx), "");
     })
 }
 
@@ -137,7 +138,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state("aa\nbˇb\ncc");
 
     // works in visuial mode
-    cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
+    cx.simulate_keystrokes(["shift-v", "down", ">"]);
     cx.assert_editor_state("aa\n    b«b\n    cˇ»c");
 }
 
@@ -153,3 +154,44 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
     assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
     cx.assert_state("aˇbc\n", Mode::Insert);
 }
+
+#[gpui::test]
+async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
+    cx.simulate_keystrokes(["/", "c", "c"]);
+
+    let search_bar = cx.workspace(|workspace, cx| {
+        workspace
+            .active_pane()
+            .read(cx)
+            .toolbar()
+            .read(cx)
+            .item_of_type::<BufferSearchBar>()
+            .expect("Buffer search bar should be deployed")
+    });
+
+    search_bar.read_with(cx.cx, |bar, cx| {
+        assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+    });
+
+    // wait for the query editor change event to fire.
+    search_bar.next_notification(&cx).await;
+
+    cx.update_editor(|editor, cx| {
+        let highlights = editor.all_background_highlights(cx);
+        assert_eq!(3, highlights.len());
+        assert_eq!(
+            DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
+            highlights[0].0
+        )
+    });
+    cx.simulate_keystrokes(["enter"]);
+
+    cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
+    cx.simulate_keystrokes(["n"]);
+    cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
+    cx.simulate_keystrokes(["shift-n"]);
+    cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
+}

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

@@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> {
         self.cx.set_state(text)
     }
 
+    #[track_caller]
     pub fn assert_state(&mut self, text: &str, mode: Mode) {
         self.assert_editor_state(text);
         assert_eq!(self.mode(), mode, "{}", self.assertion_context());

crates/vim/src/vim.rs 🔗

@@ -14,8 +14,8 @@ use anyhow::Result;
 use collections::CommandPaletteFilter;
 use editor::{Bias, Editor, EditorMode, Event};
 use gpui::{
-    actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
-    WindowContext,
+    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
+    Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use language::CursorShape;
 use motion::Motion;
@@ -90,7 +90,10 @@ pub fn init(cx: &mut AppContext) {
 }
 
 pub fn observe_keystrokes(cx: &mut WindowContext) {
-    cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
+    cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
+        if result == &MatchResult::Pending {
+            return true;
+        }
         if let Some(handled_by) = handled_by {
             // Keystroke is handled by the vim system, so continue forward
             if handled_by.namespace() == "vim" {
@@ -243,10 +246,14 @@ impl Vim {
 
         match Vim::read(cx).active_operator() {
             Some(Operator::FindForward { before }) => {
-                motion::motion(Motion::FindForward { before, text }, cx)
+                let find = Motion::FindForward { before, text };
+                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                motion::motion(find, cx)
             }
             Some(Operator::FindBackward { after }) => {
-                motion::motion(Motion::FindBackward { after, text }, cx)
+                let find = Motion::FindBackward { after, text };
+                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                motion::motion(find, cx)
             }
             Some(Operator::Replace) => match Vim::read(cx).state.mode {
                 Mode::Normal => normal_replace(text, cx),
@@ -295,22 +302,37 @@ impl Vim {
             if self.enabled && editor.mode() == EditorMode::Full {
                 editor.set_cursor_shape(cursor_shape, cx);
                 editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
+                editor.set_collapse_matches(true);
                 editor.set_input_enabled(!state.vim_controlled());
                 editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
                 let context_layer = state.keymap_context_layer();
                 editor.set_keymap_context_layer::<Self>(context_layer, cx);
             } else {
-                Self::unhook_vim_settings(editor, cx);
+                // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
+                // but we need collapse_matches to persist when the search bar is focused.
+                editor.set_collapse_matches(false);
+                self.unhook_vim_settings(editor, cx);
             }
         });
     }
 
-    fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+    fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
         editor.set_cursor_shape(CursorShape::Bar, cx);
         editor.set_clip_at_line_ends(false, cx);
         editor.set_input_enabled(true);
         editor.selections.line_mode = false;
-        editor.remove_keymap_context_layer::<Self>(cx);
+
+        // we set the VimEnabled context on all editors so that we
+        // can distinguish between vim mode and non-vim mode in the BufferSearchBar.
+        // This is a bit of a hack, but currently the search crate does not depend on vim,
+        // and it seems nice to keep it that way.
+        if self.enabled {
+            let mut context = KeymapContext::default();
+            context.add_identifier("VimEnabled");
+            editor.set_keymap_context_layer::<Self>(context, cx)
+        } else {
+            editor.remove_keymap_context_layer::<Self>(cx);
+        }
     }
 }
 

crates/vim/src/visual.rs 🔗

@@ -58,7 +58,9 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
 
 pub fn visual_object(object: Object, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
-        if let Operator::Object { around } = vim.pop_operator(cx) {
+        if let Some(Operator::Object { around }) = vim.active_operator() {
+            vim.pop_operator(cx);
+
             vim.update_active_editor(cx, |editor, cx| {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_with(|map, selection| {

crates/vim/test_data/test_comma_semicolon.json 🔗

@@ -0,0 +1,17 @@
+{"Put":{"state":"ˇone two three four"}}
+{"Key":"f"}
+{"Key":"o"}
+{"Get":{"state":"one twˇo three four","mode":"Normal"}}
+{"Key":","}
+{"Get":{"state":"ˇone two three four","mode":"Normal"}}
+{"Key":"2"}
+{"Key":";"}
+{"Get":{"state":"one two three fˇour","mode":"Normal"}}
+{"Key":"shift-t"}
+{"Key":"e"}
+{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
+{"Key":"3"}
+{"Key":";"}
+{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
+{"Key":","}
+{"Get":{"state":"one two thˇree four","mode":"Normal"}}

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -120,7 +120,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
             update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {

crates/workspace/src/item.rs 🔗

@@ -5,11 +5,15 @@ use crate::{
 use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
 use anyhow::Result;
 use client::{proto, Client};
+use gpui::geometry::vector::Vector2F;
 use gpui::{
     fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -27,6 +31,49 @@ use std::{
 };
 use theme::Theme;
 
+#[derive(Deserialize)]
+pub struct ItemSettings {
+    pub git_status: bool,
+    pub close_position: ClosePosition,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ClosePosition {
+    Left,
+    #[default]
+    Right,
+}
+
+impl ClosePosition {
+    pub fn right(&self) -> bool {
+        match self {
+            ClosePosition::Left => false,
+            ClosePosition::Right => true,
+        }
+    }
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ItemSettingsContent {
+    git_status: Option<bool>,
+    close_position: Option<ClosePosition>,
+}
+
+impl Setting for ItemSettings {
+    const KEY: Option<&'static str> = Some("tabs");
+
+    type FileContent = ItemSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
 #[derive(Eq, PartialEq, Hash, Debug)]
 pub enum ItemEvent {
     CloseItem,
@@ -157,6 +204,9 @@ pub trait Item: View {
     fn show_toolbar(&self) -> bool {
         true
     }
+    fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+        None
+    }
 }
 
 pub trait ItemHandle: 'static + fmt::Debug {
@@ -225,6 +275,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
     fn serialized_item_kind(&self) -> Option<&'static str>;
     fn show_toolbar(&self, cx: &AppContext) -> bool;
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F>;
 }
 
 pub trait WeakItemHandle {
@@ -569,6 +620,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     fn show_toolbar(&self, cx: &AppContext) -> bool {
         self.read(cx).show_toolbar()
     }
+
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+        self.read(cx).pixel_position_of_cursor()
+    }
 }
 
 impl From<Box<dyn ItemHandle>> for AnyViewHandle {

crates/workspace/src/pane.rs 🔗

@@ -3,14 +3,16 @@ mod dragged_item_receiver;
 use super::{ItemHandle, SplitDirection};
 pub use crate::toolbar::Toolbar;
 use crate::{
-    item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile,
-    NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
+    item::{ItemSettings, WeakItemHandle},
+    notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom,
+    Workspace, WorkspaceSettings,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
 use drag_and_drop::{DragAndDrop, Draggable};
 use dragged_item_receiver::dragged_item_receiver;
+use fs::repository::GitFileStatus;
 use futures::StreamExt;
 use gpui::{
     actions,
@@ -23,8 +25,8 @@ use gpui::{
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    LayoutContext, ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    LayoutContext, ModelHandle, MouseRegion, PaintContext, Quad, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle, WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -540,6 +542,12 @@ impl Pane {
         self.items.get(self.active_item_index).cloned()
     }
 
+    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+        self.items
+            .get(self.active_item_index)?
+            .pixel_position_of_cursor(cx)
+    }
+
     pub fn item_for_entry(
         &self,
         entry_id: ProjectEntryId,
@@ -866,6 +874,7 @@ impl Pane {
                 .paths_by_item
                 .get(&item.id())
                 .and_then(|(_, abs_path)| abs_path.clone());
+
             self.nav_history
                 .0
                 .borrow_mut()
@@ -1157,6 +1166,11 @@ impl Pane {
             .zip(self.tab_details(cx))
             .enumerate()
         {
+            let git_status = item
+                .project_path(cx)
+                .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
+                .and_then(|entry| entry.git_status());
+
             let detail = if detail == 0 { None } else { Some(detail) };
             let tab_active = ix == self.active_item_index;
 
@@ -1174,9 +1188,21 @@ impl Pane {
                         let tab_tooltip_text =
                             item.tab_tooltip_text(cx).map(|text| text.into_owned());
 
+                        let mut tab_style = theme
+                            .workspace
+                            .tab_bar
+                            .tab_style(pane_active, tab_active)
+                            .clone();
+                        let should_show_status = settings::get::<ItemSettings>(cx).git_status;
+                        if should_show_status && git_status != None {
+                            tab_style.label.text.color = match git_status.unwrap() {
+                                GitFileStatus::Added => tab_style.git.inserted,
+                                GitFileStatus::Modified => tab_style.git.modified,
+                                GitFileStatus::Conflict => tab_style.git.conflict,
+                            };
+                        }
+
                         move |mouse_state, cx| {
-                            let tab_style =
-                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
                             let hovered = mouse_state.hovered();
 
                             enum Tab {}
@@ -1188,7 +1214,7 @@ impl Pane {
                                         ix == 0,
                                         detail,
                                         hovered,
-                                        tab_style,
+                                        &tab_style,
                                         cx,
                                     )
                                 })
@@ -1350,81 +1376,94 @@ impl Pane {
             container.border.left = false;
         }
 
-        Flex::row()
-            .with_child({
-                let diameter = 7.0;
-                let icon_color = if item.has_conflict(cx) {
-                    Some(tab_style.icon_conflict)
-                } else if item.is_dirty(cx) {
-                    Some(tab_style.icon_dirty)
-                } else {
-                    None
-                };
+        let buffer_jewel_element = {
+            let diameter = 7.0;
+            let icon_color = if item.has_conflict(cx) {
+                Some(tab_style.icon_conflict)
+            } else if item.is_dirty(cx) {
+                Some(tab_style.icon_dirty)
+            } else {
+                None
+            };
 
-                Canvas::new(move |scene, bounds, _, _, _| {
-                    if let Some(color) = icon_color {
-                        let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
-                        scene.push_quad(Quad {
-                            bounds: square,
-                            background: Some(color),
-                            border: Default::default(),
-                            corner_radius: diameter / 2.,
-                        });
-                    }
-                })
-                .constrained()
-                .with_width(diameter)
-                .with_height(diameter)
-                .aligned()
+            Canvas::new(move |scene, bounds, _, _, _| {
+                if let Some(color) = icon_color {
+                    let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
+                    scene.push_quad(Quad {
+                        bounds: square,
+                        background: Some(color),
+                        border: Default::default(),
+                        corner_radius: diameter / 2.,
+                    });
+                }
             })
-            .with_child(title.aligned().contained().with_style(ContainerStyle {
-                margin: Margin {
-                    left: tab_style.spacing,
-                    right: tab_style.spacing,
-                    ..Default::default()
-                },
+            .constrained()
+            .with_width(diameter)
+            .with_height(diameter)
+            .aligned()
+        };
+
+        let title_element = title.aligned().contained().with_style(ContainerStyle {
+            margin: Margin {
+                left: tab_style.spacing,
+                right: tab_style.spacing,
                 ..Default::default()
-            }))
-            .with_child(
-                if hovered {
-                    let item_id = item.id();
-                    enum TabCloseButton {}
-                    let icon = Svg::new("icons/x_mark_8.svg");
-                    MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
-                        if mouse_state.hovered() {
-                            icon.with_color(tab_style.icon_close_active)
-                        } else {
-                            icon.with_color(tab_style.icon_close)
-                        }
-                    })
-                    .with_padding(Padding::uniform(4.))
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, {
-                        let pane = pane.clone();
-                        move |_, _, cx| {
-                            let pane = pane.clone();
-                            cx.window_context().defer(move |cx| {
-                                if let Some(pane) = pane.upgrade(cx) {
-                                    pane.update(cx, |pane, cx| {
-                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
-                                    });
-                                }
+            },
+            ..Default::default()
+        });
+
+        let close_element = if hovered {
+            let item_id = item.id();
+            enum TabCloseButton {}
+            let icon = Svg::new("icons/x_mark_8.svg");
+            MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
+                if mouse_state.hovered() {
+                    icon.with_color(tab_style.icon_close_active)
+                } else {
+                    icon.with_color(tab_style.icon_close)
+                }
+            })
+            .with_padding(Padding::uniform(4.))
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, {
+                let pane = pane.clone();
+                move |_, _, cx| {
+                    let pane = pane.clone();
+                    cx.window_context().defer(move |cx| {
+                        if let Some(pane) = pane.upgrade(cx) {
+                            pane.update(cx, |pane, cx| {
+                                pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
                             });
                         }
-                    })
-                    .into_any_named("close-tab-icon")
-                    .constrained()
-                } else {
-                    Empty::new().constrained()
+                    });
                 }
-                .with_width(tab_style.close_icon_width)
-                .aligned(),
-            )
-            .contained()
-            .with_style(container)
+            })
+            .into_any_named("close-tab-icon")
             .constrained()
-            .with_height(tab_style.height)
-            .into_any()
+        } else {
+            Empty::new().constrained()
+        }
+        .with_width(tab_style.close_icon_width)
+        .aligned();
+
+        let close_right = settings::get::<ItemSettings>(cx).close_position.right();
+
+        if close_right {
+            Flex::row()
+                .with_child(buffer_jewel_element)
+                .with_child(title_element)
+                .with_child(close_element)
+        } else {
+            Flex::row()
+                .with_child(close_element)
+                .with_child(title_element)
+                .with_child(buffer_jewel_element)
+        }
+        .contained()
+        .with_style(container)
+        .constrained()
+        .with_height(tab_style.height)
+        .into_any()
     }
 
     pub fn render_tab_bar_button<
@@ -1857,7 +1896,7 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let background = theme::current(cx).editor.background;
 

crates/workspace/src/pane_group.rs 🔗

@@ -54,6 +54,20 @@ impl PaneGroup {
         }
     }
 
+    pub fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
+        match &self.root {
+            Member::Pane(_) => None,
+            Member::Axis(axis) => axis.bounding_box_for_pane(pane),
+        }
+    }
+
+    pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
+        match &self.root {
+            Member::Pane(pane) => Some(pane),
+            Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
+        }
+    }
+
     /// Returns:
     /// - Ok(true) if it found and removed a pane
     /// - Ok(false) if it found but did not remove the pane
@@ -309,15 +323,18 @@ pub(crate) struct PaneAxis {
     pub axis: Axis,
     pub members: Vec<Member>,
     pub flexes: Rc<RefCell<Vec<f32>>>,
+    pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
 }
 
 impl PaneAxis {
     pub fn new(axis: Axis, members: Vec<Member>) -> Self {
         let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
+        let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
         Self {
             axis,
             members,
             flexes,
+            bounding_boxes,
         }
     }
 
@@ -326,10 +343,12 @@ impl PaneAxis {
         debug_assert!(members.len() == flexes.len());
 
         let flexes = Rc::new(RefCell::new(flexes));
+        let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
         Self {
             axis,
             members,
             flexes,
+            bounding_boxes,
         }
     }
 
@@ -398,7 +417,9 @@ impl PaneAxis {
             }
 
             if self.members.len() == 1 {
-                Ok(self.members.pop())
+                let result = self.members.pop();
+                *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+                Ok(result)
             } else {
                 Ok(None)
             }
@@ -407,6 +428,44 @@ impl PaneAxis {
         }
     }
 
+    fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
+        debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+
+        for (idx, member) in self.members.iter().enumerate() {
+            match member {
+                Member::Pane(found) => {
+                    if pane == found {
+                        return self.bounding_boxes.borrow()[idx];
+                    }
+                }
+                Member::Axis(axis) => {
+                    if let Some(rect) = axis.bounding_box_for_pane(pane) {
+                        return Some(rect);
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
+        debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+
+        let bounding_boxes = self.bounding_boxes.borrow();
+
+        for (idx, member) in self.members.iter().enumerate() {
+            if let Some(coordinates) = bounding_boxes[idx] {
+                if coordinates.contains_point(coordinate) {
+                    return match member {
+                        Member::Pane(found) => Some(found),
+                        Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
+                    };
+                }
+            }
+        }
+        None
+    }
+
     fn render(
         &self,
         project: &ModelHandle<Project>,
@@ -421,7 +480,12 @@ impl PaneAxis {
     ) -> AnyElement<Workspace> {
         debug_assert!(self.members.len() == self.flexes.borrow().len());
 
-        let mut pane_axis = PaneAxisElement::new(self.axis, basis, self.flexes.clone());
+        let mut pane_axis = PaneAxisElement::new(
+            self.axis,
+            basis,
+            self.flexes.clone(),
+            self.bounding_boxes.clone(),
+        );
         let mut active_pane_ix = None;
 
         let mut members = self.members.iter().enumerate().peekable();
@@ -529,8 +593,8 @@ mod element {
         },
         json::{self, ToJson},
         platform::{CursorStyle, MouseButton},
-        AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, RectFExt,
-        SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
+        AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, PaintContext,
+        RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
     };
 
     use crate::{
@@ -544,14 +608,21 @@ mod element {
         active_pane_ix: Option<usize>,
         flexes: Rc<RefCell<Vec<f32>>>,
         children: Vec<AnyElement<Workspace>>,
+        bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
     }
 
     impl PaneAxisElement {
-        pub fn new(axis: Axis, basis: usize, flexes: Rc<RefCell<Vec<f32>>>) -> Self {
+        pub fn new(
+            axis: Axis,
+            basis: usize,
+            flexes: Rc<RefCell<Vec<f32>>>,
+            bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
+        ) -> Self {
             Self {
                 axis,
                 basis,
                 flexes,
+                bounding_boxes,
                 active_pane_ix: None,
                 children: Default::default(),
             }
@@ -694,7 +765,7 @@ mod element {
             visible_bounds: RectF,
             remaining_space: &mut Self::LayoutState,
             view: &mut Workspace,
-            cx: &mut ViewContext<Workspace>,
+            cx: &mut PaintContext<Workspace>,
         ) -> Self::PaintState {
             let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
             let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
@@ -706,11 +777,16 @@ mod element {
 
             let mut child_origin = bounds.origin();
 
+            let mut bounding_boxes = self.bounding_boxes.borrow_mut();
+            bounding_boxes.clear();
+
             let mut children_iter = self.children.iter_mut().enumerate().peekable();
             while let Some((ix, child)) = children_iter.next() {
                 let child_start = child_origin.clone();
                 child.paint(scene, child_origin, visible_bounds, view, cx);
 
+                bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
+
                 match self.axis {
                     Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
@@ -750,63 +826,79 @@ mod element {
                     let child_size = child.size();
                     let next_child_size = next_child.size();
                     let drag_bounds = visible_bounds.clone();
-                    let flexes = self.flexes.clone();
-                    let current_flex = flexes.borrow()[ix];
+                    let flexes = self.flexes.borrow();
+                    let current_flex = flexes[ix];
                     let next_ix = *next_ix;
-                    let next_flex = flexes.borrow()[next_ix];
+                    let next_flex = flexes[next_ix];
+                    drop(flexes);
                     enum ResizeHandle {}
                     let mut mouse_region = MouseRegion::new::<ResizeHandle>(
                         cx.view_id(),
                         self.basis + ix,
                         handle_bounds,
                     );
-                    mouse_region = mouse_region.on_drag(
-                        MouseButton::Left,
-                        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;
+                    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();
                             }
-
-                            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,
-                                );
+                        })
+                        .on_click(MouseButton::Left, {
+                            let flexes = self.flexes.clone();
+                            move |e, v: &mut Workspace, cx| {
+                                if e.click_count >= 2 {
+                                    let mut borrow = flexes.borrow_mut();
+                                    *borrow = vec![1.; borrow.len()];
+                                    v.schedule_serialize(cx);
+                                    cx.notify();
+                                }
                             }
-
-                            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();
-                        },
-                    );
+                        });
                     scene.push_mouse_region(mouse_region);
 
                     scene.pop_stacking_context();

crates/workspace/src/searchable.rs 🔗

@@ -37,7 +37,11 @@ pub trait SearchableItem: Item {
             regex: true,
         }
     }
-    fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<SearchEvent>;
     fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
     fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
     fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
@@ -47,29 +51,25 @@ pub trait SearchableItem: Item {
         matches: Vec<Self::Match>,
         cx: &mut ViewContext<Self>,
     );
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Self::Match>,
-        mut current_index: usize,
+        current_index: usize,
         direction: Direction,
+        count: usize,
         _: &mut ViewContext<Self>,
     ) -> usize {
         match direction {
             Direction::Prev => {
-                if current_index == 0 {
-                    matches.len() - 1
-                } else {
-                    current_index - 1
-                }
-            }
-            Direction::Next => {
-                current_index += 1;
-                if current_index == matches.len() {
-                    0
+                let count = count % matches.len();
+                if current_index >= count {
+                    current_index - count
                 } else {
-                    current_index
+                    matches.len() - (count - current_index)
                 }
             }
+            Direction::Next => (current_index + count) % matches.len(),
         }
     }
     fn find_matches(
@@ -102,11 +102,13 @@ pub trait SearchableItemHandle: ItemHandle {
         matches: &Vec<Box<dyn Any + Send>>,
         cx: &mut WindowContext,
     );
+    fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
     fn match_index_for_direction(
         &self,
         matches: &Vec<Box<dyn Any + Send>>,
         current_index: usize,
         direction: Direction,
+        count: usize,
         cx: &mut WindowContext,
     ) -> usize;
     fn find_matches(
@@ -139,8 +141,9 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         cx: &mut WindowContext,
         handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
     ) -> Subscription {
-        cx.subscribe(self, move |_, event, cx| {
-            if let Some(search_event) = T::to_search_event(event) {
+        cx.subscribe(self, move |handle, event, cx| {
+            let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx));
+            if let Some(search_event) = search_event {
                 handler(search_event, cx)
             }
         })
@@ -165,16 +168,23 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| this.activate_match(index, matches, cx));
     }
+
+    fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
+        let matches = downcast_matches(matches);
+        self.update(cx, |this, cx| this.select_matches(matches, cx));
+    }
+
     fn match_index_for_direction(
         &self,
         matches: &Vec<Box<dyn Any + Send>>,
         current_index: usize,
         direction: Direction,
+        count: usize,
         cx: &mut WindowContext,
     ) -> usize {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| {
-            this.match_index_for_direction(&matches, current_index, direction, cx)
+            this.match_index_for_direction(&matches, current_index, direction, count, cx)
         })
     }
     fn find_matches(

crates/workspace/src/status_bar.rs 🔗

@@ -8,8 +8,8 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription,
-    View, ViewContext, ViewHandle, WindowContext,
+    AnyElement, AnyViewHandle, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    Subscription, View, ViewContext, ViewHandle, WindowContext,
 };
 
 pub trait StatusItemView: View {
@@ -177,7 +177,7 @@ impl Element<StatusBar> for StatusBarElement {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut StatusBar,
-        cx: &mut ViewContext<StatusBar>,
+        cx: &mut PaintContext<StatusBar>,
     ) -> Self::PaintState {
         let origin_y = bounds.upper_right().y();
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();

crates/workspace/src/workspace.rs 🔗

@@ -141,6 +141,7 @@ actions!(
         ToggleLeftDock,
         ToggleRightDock,
         ToggleBottomDock,
+        CloseAllDocks,
     ]
 );
 
@@ -152,6 +153,9 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePane(pub usize);
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ActivatePaneInDirection(pub SplitDirection);
+
 #[derive(Deserialize)]
 pub struct Toast {
     id: usize,
@@ -197,12 +201,13 @@ impl Clone for Toast {
     }
 }
 
-impl_actions!(workspace, [ActivatePane, Toast]);
+impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
 
 pub type WorkspaceId = i64;
 
 pub fn init_settings(cx: &mut AppContext) {
     settings::register::<WorkspaceSettings>(cx);
+    settings::register::<item::ItemSettings>(cx);
 }
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
@@ -261,6 +266,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
         workspace.activate_next_pane(cx)
     });
+
+    cx.add_action(
+        |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
+            workspace.activate_pane_in_direction(action.0, cx)
+        },
+    );
+
     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
     });
@@ -270,6 +282,9 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
         workspace.toggle_dock(DockPosition::Bottom, cx);
     });
+    cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+        workspace.close_all_docks(cx);
+    });
     cx.add_action(Workspace::activate_pane_at_index);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         workspace.reopen_closed_item(cx).detach();
@@ -497,7 +512,7 @@ pub struct Workspace {
     follower_states_by_leader: FollowerStatesByLeader,
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
-    active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
+    active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
     app_state: Arc<AppState>,
@@ -883,6 +898,18 @@ impl Workspace {
     pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
     where
         T::Event: std::fmt::Debug,
+    {
+        self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
+    }
+
+    pub fn add_panel_with_extra_event_handler<T: Panel, F>(
+        &mut self,
+        panel: ViewHandle<T>,
+        cx: &mut ViewContext<Self>,
+        handler: F,
+    ) where
+        T::Event: std::fmt::Debug,
+        F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
     {
         let dock = match panel.position(cx) {
             DockPosition::Left => &self.left_dock,
@@ -950,6 +977,8 @@ impl Workspace {
                     }
                     this.update_active_view_for_followers(cx);
                     cx.notify();
+                } else {
+                    handler(this, &panel, event, cx)
                 }
             }
         }));
@@ -1402,45 +1431,65 @@ impl Workspace {
         // Sort the paths to ensure we add worktrees for parents before their children.
         abs_paths.sort_unstable();
         cx.spawn(|this, mut cx| async move {
-            let mut project_paths = Vec::new();
-            for path in &abs_paths {
-                if let Some(project_path) = this
+            let mut tasks = Vec::with_capacity(abs_paths.len());
+            for abs_path in &abs_paths {
+                let project_path = match this
                     .update(&mut cx, |this, cx| {
-                        Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
+                        Workspace::project_path_for_path(
+                            this.project.clone(),
+                            abs_path,
+                            visible,
+                            cx,
+                        )
                     })
                     .log_err()
                 {
-                    project_paths.push(project_path.await.log_err());
-                } else {
-                    project_paths.push(None);
-                }
-            }
+                    Some(project_path) => project_path.await.log_err(),
+                    None => None,
+                };
 
-            let tasks = abs_paths
-                .iter()
-                .cloned()
-                .zip(project_paths.into_iter())
-                .map(|(abs_path, project_path)| {
-                    let this = this.clone();
-                    cx.spawn(|mut cx| {
-                        let fs = fs.clone();
-                        async move {
-                            let (_worktree, project_path) = project_path?;
-                            if fs.is_file(&abs_path).await {
-                                Some(
-                                    this.update(&mut cx, |this, cx| {
-                                        this.open_path(project_path, None, true, cx)
+                let this = this.clone();
+                let task = cx.spawn(|mut cx| {
+                    let fs = fs.clone();
+                    let abs_path = abs_path.clone();
+                    async move {
+                        let (worktree, project_path) = project_path?;
+                        if fs.is_file(&abs_path).await {
+                            Some(
+                                this.update(&mut cx, |this, cx| {
+                                    this.open_path(project_path, None, true, cx)
+                                })
+                                .log_err()?
+                                .await,
+                            )
+                        } else {
+                            this.update(&mut cx, |workspace, cx| {
+                                let worktree = worktree.read(cx);
+                                let worktree_abs_path = worktree.abs_path();
+                                let entry_id = if abs_path == worktree_abs_path.as_ref() {
+                                    worktree.root_entry()
+                                } else {
+                                    abs_path
+                                        .strip_prefix(worktree_abs_path.as_ref())
+                                        .ok()
+                                        .and_then(|relative_path| {
+                                            worktree.entry_for_path(relative_path)
+                                        })
+                                }
+                                .map(|entry| entry.id);
+                                if let Some(entry_id) = entry_id {
+                                    workspace.project().update(cx, |_, cx| {
+                                        cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
                                     })
-                                    .log_err()?
-                                    .await,
-                                )
-                            } else {
-                                None
-                            }
+                                }
+                            })
+                            .log_err()?;
+                            None
                         }
-                    })
-                })
-                .collect::<Vec<_>>();
+                    }
+                });
+                tasks.push(task);
+            }
 
             futures::future::join_all(tasks).await
         })
@@ -1659,6 +1708,20 @@ impl Workspace {
         self.serialize_workspace(cx);
     }
 
+    pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
+        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
+
+        for dock in docks {
+            dock.update(cx, |dock, cx| {
+                dock.set_open(false, cx);
+            });
+        }
+
+        cx.focus_self();
+        cx.notify();
+        self.serialize_workspace(cx);
+    }
+
     /// Transfer focus to the panel of the given type.
     pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
         self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
@@ -1821,6 +1884,13 @@ impl Workspace {
             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
 
+    pub fn split_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+        new_pane.update(cx, move |new_pane, cx| {
+            new_pane.add_item(item, true, true, None, cx)
+        })
+    }
+
     pub fn open_abs_path(
         &mut self,
         abs_path: PathBuf,
@@ -1851,6 +1921,21 @@ impl Workspace {
         })
     }
 
+    pub fn split_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        let project_path_task =
+            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+        cx.spawn(|this, mut cx| async move {
+            let (_, path) = project_path_task.await?;
+            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+                .await
+        })
+    }
+
     pub fn open_path(
         &mut self,
         path: impl Into<ProjectPath>,
@@ -1876,6 +1961,38 @@ impl Workspace {
         })
     }
 
+    pub fn split_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+            self.panes
+                .first()
+                .expect("There must be an active pane")
+                .downgrade()
+        });
+
+        if let Member::Pane(center_pane) = &self.center.root {
+            if center_pane.read(cx).items_len() == 0 {
+                return self.open_path(path, Some(pane), true, cx);
+            }
+        }
+
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            this.update(&mut cx, move |this, cx| -> Option<_> {
+                let pane = pane.upgrade(cx)?;
+                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                new_pane.update(cx, |new_pane, cx| {
+                    Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+                })
+            })
+            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+        })
+    }
+
     pub(crate) fn load_path(
         &mut self,
         path: ProjectPath,
@@ -1926,6 +2043,30 @@ impl Workspace {
         item
     }
 
+    pub fn split_project_item<T>(
+        &mut self,
+        project_item: ModelHandle<T::Item>,
+        cx: &mut ViewContext<Self>,
+    ) -> ViewHandle<T>
+    where
+        T: ProjectItem,
+    {
+        use project::Item as _;
+
+        let entry_id = project_item.read(cx).entry_id(cx);
+        if let Some(item) = entry_id
+            .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
+            .and_then(|item| item.downcast())
+        {
+            self.activate_item(&item, cx);
+            return item;
+        }
+
+        let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
+        self.split_item(Box::new(item.clone()), cx);
+        item
+    }
+
     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
             self.active_pane.update(cx, |pane, cx| {
@@ -1953,7 +2094,7 @@ impl Workspace {
         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
             cx.focus(&pane);
         } else {
-            self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
         }
     }
 
@@ -1975,6 +2116,37 @@ impl Workspace {
         }
     }
 
+    pub fn activate_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
+            Some(coordinates) => coordinates,
+            None => {
+                return;
+            }
+        };
+        let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
+        let center = match cursor {
+            Some(cursor) if bounding_box.contains_point(cursor) => cursor,
+            _ => bounding_box.center(),
+        };
+
+        let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
+
+        let target = match direction {
+            SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
+            SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
+            SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
+            SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
+        };
+
+        if let Some(pane) = self.center.pane_at_pixel_position(target) {
+            cx.focus(pane);
+        }
+    }
+
     fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.active_pane != pane {
             self.active_pane = pane.clone();
@@ -2006,7 +2178,7 @@ impl Workspace {
         match event {
             pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
             pane::Event::Split(direction) => {
-                self.split_pane(pane, *direction, cx);
+                self.split_and_clone(pane, *direction, cx);
             }
             pane::Event::Remove => self.remove_pane(pane, cx),
             pane::Event::ActivateItem { local } => {
@@ -2057,6 +2229,20 @@ impl Workspace {
     }
 
     pub fn split_pane(
+        &mut self,
+        pane_to_split: ViewHandle<Pane>,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> ViewHandle<Pane> {
+        let new_pane = self.add_pane(cx);
+        self.center
+            .split(&pane_to_split, &new_pane, split_direction)
+            .unwrap();
+        cx.notify();
+        new_pane
+    }
+
+    pub fn split_and_clone(
         &mut self,
         pane: ViewHandle<Pane>,
         direction: SplitDirection,
@@ -2937,6 +3123,7 @@ impl Workspace {
                     axis,
                     members,
                     flexes,
+                    bounding_boxes: _,
                 }) => SerializedPaneGroup::Group {
                     axis: *axis,
                     children: members
@@ -3410,27 +3597,11 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
             if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
                 workspace.show_notification_once(0, cx, |cx| {
                     cx.add_view(|_| {
-                        MessageNotification::new("Failed to load any database file.")
+                        MessageNotification::new("Failed to load the database file.")
                             .with_click_message("Click to let us know about this error")
                             .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
                     })
                 });
-            } else {
-                let backup_path = (*db::BACKUP_DB_PATH).read();
-                if let Some(backup_path) = backup_path.clone() {
-                    workspace.show_notification_once(1, cx, move |cx| {
-                        cx.add_view(move |_| {
-                            MessageNotification::new(format!(
-                                "Database file was corrupted. Old database backed up to {}",
-                                backup_path.display()
-                            ))
-                            .with_click_message("Click to show old database in finder")
-                            .on_click(move |cx| {
-                                cx.platform().open_url(&backup_path.to_string_lossy())
-                            })
-                        })
-                    });
-                }
             }
         })
         .log_err();
@@ -4246,7 +4417,7 @@ mod tests {
             });
 
             workspace
-                .split_pane(left_pane.clone(), SplitDirection::Right, cx)
+                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
                 .unwrap();
 
             left_pane

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.96.0"
+version = "0.97.0"
 publish = false
 
 [lib]
@@ -104,26 +104,31 @@ thiserror.workspace = true
 tiny_http = "0.8"
 toml.workspace = true
 tree-sitter.workspace = true
-tree-sitter-c = "0.20.1"
-tree-sitter-cpp = "0.20.0"
-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-embedded-template = "0.20.0"
-tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
-tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
-tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
-tree-sitter-rust = "0.20.3"
-tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
-tree-sitter-python = "0.20.2"
-tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
-tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
-tree-sitter-ruby = "0.20.0"
-tree-sitter-html = "0.19.0"
-tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "ca8af220aaf2a80aaf609bfb0df193817e4f064b"}
-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-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca"}
-tree-sitter-lua = "0.0.14"
+tree-sitter-bash.workspace = true
+tree-sitter-c.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-css.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-elm.workspace = true
+tree-sitter-embedded-template.workspace = true
+tree-sitter-glsl.workspace = true
+tree-sitter-go.workspace = true
+tree-sitter-heex.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-markdown.workspace = true
+tree-sitter-python.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-typescript.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-php.workspace = true
+tree-sitter-scheme.workspace = true
+tree-sitter-svelte.workspace = true
+tree-sitter-racket.workspace = true
+tree-sitter-yaml.workspace = true
+tree-sitter-lua.workspace = true
+
 url = "2.2"
 urlencoding = "2.1.2"
 uuid = { version = "1.1.2", features = ["v4"] }

crates/zed/src/languages.rs 🔗

@@ -13,6 +13,7 @@ mod json;
 #[cfg(feature = "plugin_runtime")]
 mod language_plugin;
 mod lua;
+mod php;
 mod python;
 mod ruby;
 mod rust;
@@ -39,6 +40,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
         languages.register(name, load_config(name), grammar, adapters, load_queries)
     };
 
+    language("bash", tree_sitter_bash::language(), vec![]);
     language(
         "c",
         tree_sitter_c::language(),
@@ -145,6 +147,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
             node_runtime.clone(),
         ))],
     );
+    language(
+        "php",
+        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![]);
 }
 
 #[cfg(any(test, feature = "test-support"))]

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

@@ -0,0 +1,8 @@
+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"]
+first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+]

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

@@ -0,0 +1,58 @@
+[
+  (string)
+  (raw_string)
+  (heredoc_body)
+  (heredoc_start)
+] @string
+
+(command_name) @function
+
+(variable_name) @property
+
+[
+  "case"
+  "do"
+  "done"
+  "elif"
+  "else"
+  "esac"
+  "export"
+  "fi"
+  "for"
+  "function"
+  "if"
+  "in"
+  "select"
+  "then"
+  "unset"
+  "until"
+  "while"
+  "local"
+  "declare"
+] @keyword
+
+(comment) @comment
+
+(function_definition name: (word) @function)
+
+(file_descriptor) @number
+
+[
+  (command_substitution)
+  (process_substitution)
+  (expansion)
+]@embedded
+
+[
+  "$"
+  "&&"
+  ">"
+  ">>"
+  "<"
+  "|"
+] @operator
+
+(
+  (command (_) @constant)
+  (#match? @constant "^-")
+)

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

@@ -0,0 +1,11 @@
+name = "Elm"
+path_suffixes = ["elm"]
+line_comment = "-- "
+block_comment = ["{- ", " -}"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]

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

@@ -0,0 +1,72 @@
+[
+    "if"
+    "then"
+    "else"
+    "let"
+    "in"
+    (case)
+    (of)
+    (backslash)
+    (as)
+    (port)
+    (exposing)
+    (alias)
+    (import)
+    (module)
+    (type)
+    (arrow)
+ ] @keyword
+
+[
+    (eq)
+    (operator_identifier)
+    (colon)
+] @operator
+
+(type_annotation(lower_case_identifier) @function)
+(port_annotation(lower_case_identifier) @function)
+(function_declaration_left(lower_case_identifier) @function.definition)
+
+(function_call_expr
+    target: (value_expr
+        name: (value_qid (lower_case_identifier) @function)))
+
+(exposed_value(lower_case_identifier) @function)
+(exposed_type(upper_case_identifier) @type)
+
+(field_access_expr(value_expr(value_qid)) @identifier)
+(lower_pattern) @variable
+(record_base_identifier) @identifier
+
+[
+    "("
+    ")"
+] @punctuation.bracket
+
+[
+    "|"
+    ","
+] @punctuation.delimiter
+
+(number_constant_expr) @constant
+
+(type_declaration(upper_case_identifier) @type)
+(type_ref) @type
+(type_alias_declaration name: (upper_case_identifier) @type)
+
+(value_expr(upper_case_qid(upper_case_identifier)) @type)
+
+[
+    (line_comment)
+    (block_comment)
+] @comment
+
+(string_escape) @string.escape
+
+[
+    (open_quote)
+    (close_quote)
+    (regular_string_part)
+    (open_char)
+    (close_char)
+] @string

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

@@ -0,0 +1,22 @@
+(type_declaration
+    (type) @context
+    (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    (type) @context
+    (alias) @context
+    name: (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    typeExpression:
+        (type_expression
+            part: (record_type
+                (field_type
+                    name: (lower_case_identifier) @name) @item)))
+
+(union_variant
+    name: (upper_case_identifier) @name) @item
+
+(value_declaration
+    functionDeclarationLeft:
+        (function_declaration_left(lower_case_identifier) @name)) @item

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

@@ -0,0 +1,9 @@
+name = "GLSL"
+path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
+line_comment = "// "
+block_comment = ["/* ", " */"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

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

@@ -0,0 +1,118 @@
+"break" @keyword
+"case" @keyword
+"const" @keyword
+"continue" @keyword
+"default" @keyword
+"do" @keyword
+"else" @keyword
+"enum" @keyword
+"extern" @keyword
+"for" @keyword
+"if" @keyword
+"inline" @keyword
+"return" @keyword
+"sizeof" @keyword
+"static" @keyword
+"struct" @keyword
+"switch" @keyword
+"typedef" @keyword
+"union" @keyword
+"volatile" @keyword
+"while" @keyword
+
+"#define" @keyword
+"#elif" @keyword
+"#else" @keyword
+"#endif" @keyword
+"#if" @keyword
+"#ifdef" @keyword
+"#ifndef" @keyword
+"#include" @keyword
+(preproc_directive) @keyword
+
+"--" @operator
+"-" @operator
+"-=" @operator
+"->" @operator
+"=" @operator
+"!=" @operator
+"*" @operator
+"&" @operator
+"&&" @operator
+"+" @operator
+"++" @operator
+"+=" @operator
+"<" @operator
+"==" @operator
+">" @operator
+"||" @operator
+
+"." @delimiter
+";" @delimiter
+
+(string_literal) @string
+(system_lib_string) @string
+
+(null) @constant
+(number_literal) @number
+(char_literal) @number
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+(type_identifier) @type
+(primitive_type) @type
+(sized_type_specifier) @type
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
+
+(identifier) @variable
+
+(comment) @comment
+; inherits: c
+
+[
+  "in"
+  "out"
+  "inout"
+  "uniform"
+  "shared"
+  "layout"
+  "attribute"
+  "varying"
+  "buffer"
+  "coherent"
+  "readonly"
+  "writeonly"
+  "precision"
+  "highp"
+  "mediump"
+  "lowp"
+  "centroid"
+  "sample"
+  "patch"
+  "smooth"
+  "flat"
+  "noperspective"
+  "invariant"
+  "precise"
+] @type.qualifier
+
+"subroutine" @keyword.function
+
+(extension_storage_class) @storageclass
+
+(
+  (identifier) @variable.builtin
+  (#match? @variable.builtin "^gl_")
+)

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

@@ -4,4 +4,4 @@ autoclose_before = ">})"
 brackets = [
     { start = "<", end = ">", close = true, newline = true },
 ]
-block_comment = ["<%#", "%>"]
+block_comment = ["<%!-- ", " --%>"]

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

@@ -1,10 +1,7 @@
 ; HEEx delimiters
 [
-  "--%>"
-  "-->"
   "/>"
   "<!"
-  "<!--"
   "<"
   "</"
   "</:"
@@ -21,6 +18,9 @@
   "<%%="
   "<%="
   "%>"
+  "--%>"
+  "-->"
+  "<!--"
 ] @keyword
 
 ; HEEx operators are highlighted as such

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

@@ -0,0 +1,133 @@
+use anyhow::{anyhow, Result};
+
+use async_trait::async_trait;
+use collections::HashMap;
+
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+
+use smol::{fs, stream::StreamExt};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct IntelephenseVersion(String);
+
+pub struct IntelephenseLspAdapter {
+    node: Arc<NodeRuntime>,
+}
+
+impl IntelephenseLspAdapter {
+    const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<NodeRuntime>) -> Self {
+        Self { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for IntelephenseLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("intelephense".into())
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(IntelephenseVersion(
+            self.node.npm_package_latest_version("intelephense").await?,
+        )) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<IntelephenseVersion>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())])
+                .await?;
+        }
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: intelephense_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp::CompletionItem,
+        _language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+    async fn language_ids(&self) -> HashMap<String, String> {
+        HashMap::from_iter([("PHP".into(), "php".into())])
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: intelephense_server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,11 @@
+name = "PHP"
+path_suffixes = ["php"]
+first_line_pattern = '^#!.*php'
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]

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

@@ -0,0 +1,123 @@
+(php_tag) @tag
+"?>" @tag
+
+; Types
+
+(primitive_type) @type.builtin
+(cast_type) @type.builtin
+(named_type (name) @type) @type
+(named_type (qualified_name) @type) @type
+
+; Functions
+
+(array_creation_expression "array" @function.builtin)
+(list_literal "list" @function.builtin)
+
+(method_declaration
+  name: (name) @function.method)
+
+(function_call_expression
+  function: [(qualified_name (name)) (name)] @function)
+
+(scoped_call_expression
+  name: (name) @function)
+
+(member_call_expression
+  name: (name) @function.method)
+
+(function_definition
+  name: (name) @function)
+
+; Member
+
+(property_element
+  (variable_name) @property)
+
+(member_access_expression
+  name: (variable_name (name)) @property)
+(member_access_expression
+  name: (name) @property)
+
+; Variables
+
+(relative_scope) @variable.builtin
+
+((name) @constant
+ (#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
+((name) @constant.builtin
+ (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
+
+((name) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+((name) @variable.builtin
+ (#eq? @variable.builtin "this"))
+
+(variable_name) @variable
+
+; Basic tokens
+[
+  (string)
+  (string_value)
+  (encapsed_string)
+  (heredoc)
+  (heredoc_body)
+  (nowdoc_body)
+] @string
+(boolean) @constant.builtin
+(null) @constant.builtin
+(integer) @number
+(float) @number
+(comment) @comment
+
+"$" @operator
+
+; Keywords
+
+"abstract" @keyword
+"as" @keyword
+"break" @keyword
+"case" @keyword
+"catch" @keyword
+"class" @keyword
+"const" @keyword
+"continue" @keyword
+"declare" @keyword
+"default" @keyword
+"do" @keyword
+"echo" @keyword
+"else" @keyword
+"elseif" @keyword
+"enum" @keyword
+"enddeclare" @keyword
+"endforeach" @keyword
+"endif" @keyword
+"endswitch" @keyword
+"endwhile" @keyword
+"extends" @keyword
+"final" @keyword
+"finally" @keyword
+"foreach" @keyword
+"function" @keyword
+"global" @keyword
+"if" @keyword
+"implements" @keyword
+"include_once" @keyword
+"include" @keyword
+"insteadof" @keyword
+"interface" @keyword
+"namespace" @keyword
+"new" @keyword
+"private" @keyword
+"protected" @keyword
+"public" @keyword
+"require_once" @keyword
+"require" @keyword
+"return" @keyword
+"static" @keyword
+"switch" @keyword
+"throw" @keyword
+"trait" @keyword
+"try" @keyword
+"use" @keyword
+"while" @keyword

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

@@ -0,0 +1,26 @@
+(class_declaration
+    "class" @context
+    name: (name) @name
+    ) @item
+
+(function_definition
+    "function" @context
+    name: (_) @name
+    ) @item
+
+
+
+(method_declaration
+    "function" @context
+    name: (_) @name
+    ) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name
+    ) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name
+    ) @item

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

@@ -0,0 +1,40 @@
+(namespace_definition
+  name: (namespace_name) @name) @module
+
+(interface_declaration
+  name: (name) @name) @definition.interface
+
+(trait_declaration
+  name: (name) @name) @definition.interface
+
+(class_declaration
+  name: (name) @name) @definition.class
+
+(class_interface_clause [(name) (qualified_name)] @name) @impl
+
+(property_declaration
+  (property_element (variable_name (name) @name))) @definition.field
+
+(function_definition
+  name: (name) @name) @definition.function
+
+(method_declaration
+  name: (name) @name) @definition.function
+
+(object_creation_expression
+  [
+    (qualified_name (name) @name)
+    (variable_name (name) @name)
+  ]) @reference.class
+
+(function_call_expression
+  function: [
+    (qualified_name (name) @name)
+    (variable_name (name)) @name
+  ]) @reference.call
+
+(scoped_call_expression
+  name: (name) @name) @reference.call
+
+(member_call_expression
+  name: (name) @name) @reference.call

crates/zed/src/main.rs 🔗

@@ -36,7 +36,7 @@ use std::{
     path::{Path, PathBuf},
     str,
     sync::{
-        atomic::{AtomicBool, Ordering},
+        atomic::{AtomicBool, AtomicU32, Ordering},
         Arc, Weak,
     },
     thread,
@@ -154,7 +154,7 @@ fn main() {
         file_finder::init(cx);
         outline::init(cx);
         project_symbols::init(cx);
-        project_panel::init(cx);
+        project_panel::init(Assets, cx);
         diagnostics::init(cx);
         search::init(cx);
         vector_store::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -166,6 +166,7 @@ fn main() {
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
         cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
             .detach();
+        watch_file_types(fs.clone(), cx);
 
         languages.set_theme(theme::current(cx).clone());
         cx.observe_global::<SettingsStore, _>({
@@ -405,11 +406,18 @@ struct PanicRequest {
     token: String,
 }
 
+static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
+
 fn init_panic_hook(app: &App, installation_id: Option<String>) {
     let is_pty = stdout_is_a_pty();
     let platform = app.platform();
 
     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(()));
+        }
+
         let app_version = ZED_APP_VERSION
             .or_else(|| platform.app_version().ok())
             .map_or("dev".to_string(), |v| v.to_string());
@@ -464,7 +472,6 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
         if is_pty {
             if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
                 eprintln!("{}", panic_data_json);
-                return;
             }
         } else {
             if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
@@ -481,6 +488,8 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
                 }
             }
         }
+
+        std::process::abort();
     }));
 }
 
@@ -677,6 +686,26 @@ async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> O
     Some(())
 }
 
+#[cfg(debug_assertions)]
+fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    cx.spawn(|mut cx| async move {
+        let mut events = fs
+            .watch(
+                "assets/icons/file_icons/file_types.json".as_ref(),
+                Duration::from_millis(100),
+            )
+            .await;
+        while (events.next().await).is_some() {
+            cx.update(|cx| {
+                cx.update_global(|file_types, _| {
+                    *file_types = project_panel::file_associations::FileAssociations::new(Assets);
+                });
+            })
+        }
+    })
+    .detach()
+}
+
 #[cfg(not(debug_assertions))]
 async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
     None
@@ -687,6 +716,9 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
     None
 }
 
+#[cfg(not(debug_assertions))]
+fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {}
+
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
@@ -887,7 +919,14 @@ pub fn dock_default_item_factory(
         })
         .notify_err(workspace, cx)?;
 
-    let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+    let terminal_view = cx.add_view(|cx| {
+        TerminalView::new(
+            terminal,
+            workspace.weak_handle(),
+            workspace.database_id(),
+            cx,
+        )
+    });
 
     Some(Box::new(terminal_view))
 }

crates/zed/src/menus.rs 🔗

@@ -93,6 +93,7 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
                 MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
                 MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
+                MenuItem::action("Close All Docks", workspace::CloseAllDocks),
                 MenuItem::submenu(Menu {
                     name: "Editor Layout",
                     items: vec![

crates/zed/src/zed.rs 🔗

@@ -338,9 +338,22 @@ pub fn initialize_workspace(
         let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
         let (project_panel, terminal_panel, assistant_panel) =
             futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
+
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
-            workspace.add_panel(project_panel, cx);
+            workspace.add_panel_with_extra_event_handler(
+                project_panel,
+                cx,
+                |workspace, _, event, cx| match event {
+                    project_panel::Event::NewSearchInDirectory { dir_entry } => {
+                        search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx)
+                    }
+                    project_panel::Event::ActivatePanel => {
+                        workspace.focus_panel::<ProjectPanel>(cx);
+                    }
+                    _ => {}
+                },
+            );
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
 
@@ -517,11 +530,7 @@ pub fn handle_keymap_file_changes(
         let mut settings_subscription = None;
         while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
             if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
-                cx.update(|cx| {
-                    cx.clear_bindings();
-                    load_default_keymap(cx);
-                    keymap_content.clone().add_to_cx(cx).log_err();
-                });
+                cx.update(|cx| reload_keymaps(cx, &keymap_content));
 
                 let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
                 drop(settings_subscription);
@@ -530,10 +539,7 @@ pub fn handle_keymap_file_changes(
                         let new_base_keymap = *settings::get::<BaseKeymap>(cx);
                         if new_base_keymap != old_base_keymap {
                             old_base_keymap = new_base_keymap.clone();
-
-                            cx.clear_bindings();
-                            load_default_keymap(cx);
-                            keymap_content.clone().add_to_cx(cx).log_err();
+                            reload_keymaps(cx, &keymap_content);
                         }
                     })
                     .detach();
@@ -544,6 +550,13 @@ pub fn handle_keymap_file_changes(
     .detach();
 }
 
+fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
+    cx.clear_bindings();
+    load_default_keymap(cx);
+    keymap_content.clone().add_to_cx(cx).log_err();
+    cx.set_menus(menus::menus());
+}
+
 fn open_local_settings_file(
     workspace: &mut Workspace,
     _: &OpenLocalSettings,
@@ -1021,7 +1034,7 @@ mod tests {
         // Split the pane with the first entry, then open the second entry again.
         workspace
             .update(cx, |w, cx| {
-                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
+                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
                 w.open_path(file2.clone(), None, true, cx)
             })
             .await
@@ -1085,8 +1098,46 @@ mod tests {
             )
             .await;
 
-        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        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();
+
+        #[track_caller]
+        fn assert_project_panel_selection(
+            workspace: &Workspace,
+            expected_worktree_path: &Path,
+            expected_entry_path: &Path,
+            cx: &AppContext,
+        ) {
+            let project_panel = [
+                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+            ]
+            .into_iter()
+            .find_map(std::convert::identity)
+            .expect("found no project panels")
+            .read(cx);
+            let (selected_worktree, selected_entry) = project_panel
+                .selected_entry(cx)
+                .expect("project panel should have a selected entry");
+            assert_eq!(
+                selected_worktree.abs_path().as_ref(),
+                expected_worktree_path,
+                "Unexpected project panel selected worktree path"
+            );
+            assert_eq!(
+                selected_entry.path.as_ref(),
+                expected_entry_path,
+                "Unexpected project panel selected entry path"
+            );
+        }
 
         // Open a file within an existing worktree.
         workspace
@@ -1095,9 +1146,10 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1118,8 +1170,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1132,7 +1185,6 @@ mod tests {
             );
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1153,8 +1205,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1167,7 +1220,6 @@ mod tests {
             );
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1188,8 +1240,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1202,7 +1255,6 @@ mod tests {
             );
 
             let visible_worktree_roots = workspace
-                .read(cx)
                 .visible_worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1216,7 +1268,6 @@ mod tests {
 
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1344,7 +1395,11 @@ mod tests {
         cx.dispatch_action(window_id, NewFile);
         workspace
             .update(cx, |workspace, cx| {
-                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                workspace.split_and_clone(
+                    workspace.active_pane().clone(),
+                    SplitDirection::Right,
+                    cx,
+                );
                 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
             })
             .await
@@ -2330,7 +2385,7 @@ mod tests {
             editor::init(cx);
             project_panel::init_settings(cx);
             pane::init(cx);
-            project_panel::init(cx);
+            project_panel::init((), cx);
             terminal_view::init(cx);
             ai::init(cx);
             app_state

script/generate-licenses 🔗

@@ -26,4 +26,4 @@ sed -i '' 's/&#x27;/'\''/g' $OUTPUT_FILE # The ` '\'' ` thing ends the string, a
 sed -i '' 's/&#x3D;/=/g' $OUTPUT_FILE
 sed -i '' 's/&#x60;/`/g' $OUTPUT_FILE
 sed -i '' 's/&lt;/</g' $OUTPUT_FILE
-sed -i '' 's/&gt;/>/g' $OUTPUT_FILE
+sed -i '' 's/&gt;/>/g' $OUTPUT_FILE

styles/src/style_tree/editor.ts 🔗

@@ -114,7 +114,7 @@ export default function editor(): any {
                             color: foreground(layer, "default"),
                         },
                         hovered: {
-                            color: foreground(layer, "variant"),
+                            color: foreground(layer, "on"),
                         },
                     },
                 },
@@ -170,6 +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),
         unnecessary_code_fade: 0.5,
         selection: theme.players[0],
         whitespace: theme.ramps.neutral(0.5).hex(),

styles/src/style_tree/feedback.ts 🔗

@@ -33,6 +33,11 @@ export default function feedback(): any {
                     background: background(theme.highest, "on", "hovered"),
                     border: border(theme.highest, "on", "hovered"),
                 },
+                disabled: {
+                    ...text(theme.highest, "mono", "on", "disabled"),
+                    background: background(theme.highest, "on", "disabled"),
+                    border: border(theme.highest, "on", "disabled"),
+                }
             },
         }),
         button_margin: 8,

styles/src/style_tree/project_panel.ts 🔗

@@ -46,9 +46,11 @@ export default function project_panel(): any {
         const base_properties = {
             height: 22,
             background: background(theme.middle),
-            icon_color: foreground(theme.middle, "variant"),
-            icon_size: 7,
-            icon_spacing: 5,
+            chevron_color: foreground(theme.middle, "variant"),
+            icon_color: with_opacity(foreground(theme.middle, "active"), 0.3),
+            chevron_size: 7,
+            icon_size: 14,
+            icon_spacing: 6,
             text: text(theme.middle, "sans", "variant", { size: "sm" }),
             status: {
                 ...git_status,
@@ -62,17 +64,17 @@ export default function project_panel(): any {
         const unselected_default_style = merge(
             base_properties,
             unselected?.default ?? {},
-            {}
+            {},
         )
         const unselected_hovered_style = merge(
             base_properties,
             { background: background(theme.middle, "hovered") },
-            unselected?.hovered ?? {}
+            unselected?.hovered ?? {},
         )
         const unselected_clicked_style = merge(
             base_properties,
             { background: background(theme.middle, "pressed") },
-            unselected?.clicked ?? {}
+            unselected?.clicked ?? {},
         )
         const selected_default_style = merge(
             base_properties,
@@ -80,7 +82,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.default ?? {}
+            selected_style?.default ?? {},
         )
         const selected_hovered_style = merge(
             base_properties,
@@ -88,7 +90,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "hovered"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.hovered ?? {}
+            selected_style?.hovered ?? {},
         )
         const selected_clicked_style = merge(
             base_properties,
@@ -96,7 +98,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "pressed"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.clicked ?? {}
+            selected_style?.clicked ?? {},
         )
 
         return toggleable({
@@ -155,7 +157,7 @@ export default function project_panel(): any {
         }),
         background: background(theme.middle),
         padding: { left: 6, right: 6, top: 0, bottom: 6 },
-        indent_width: 12,
+        indent_width: 20,
         entry: default_entry,
         dragged_entry: {
             ...default_entry.inactive.default,
@@ -173,7 +175,7 @@ export default function project_panel(): any {
                 default: {
                     icon_color: foreground(theme.middle, "variant"),
                 },
-            }
+            },
         ),
         cut_entry: entry(
             {
@@ -188,7 +190,7 @@ export default function project_panel(): any {
                         size: "sm",
                     }),
                 },
-            }
+            },
         ),
         filename_editor: {
             background: background(theme.middle, "on"),

styles/src/style_tree/search.ts 🔗

@@ -83,6 +83,35 @@ export default function search(): any {
                 },
             },
         }),
+        action_button: interactive({
+            base: {
+                ...text(theme.highest, "mono", "on"),
+                background: background(theme.highest, "on"),
+                corner_radius: 6,
+                border: border(theme.highest, "on"),
+                margin: {
+                    right: 4,
+                },
+                padding: {
+                    bottom: 2,
+                    left: 10,
+                    right: 10,
+                    top: 2,
+                },
+            },
+            state: {
+                hovered: {
+                    ...text(theme.highest, "mono", "on", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                    border: border(theme.highest, "on", "hovered"),
+                },
+                clicked: {
+                    ...text(theme.highest, "mono", "on", "pressed"),
+                    background: background(theme.highest, "on", "pressed"),
+                    border: border(theme.highest, "on", "pressed"),
+                },
+            },
+        }),
         editor,
         invalid_editor: {
             ...editor,

styles/src/style_tree/tab_bar.ts 🔗

@@ -6,6 +6,8 @@ import { useTheme } from "../common"
 export default function tab_bar(): any {
     const theme = useTheme()
 
+    const { is_light } = theme
+
     const height = 32
 
     const active_layer = theme.highest
@@ -38,6 +40,18 @@ export default function tab_bar(): any {
         icon_conflict: foreground(layer, "warning"),
         icon_dirty: foreground(layer, "accent"),
 
+        git: {
+            modified: is_light
+                ? theme.ramps.yellow(0.6).hex()
+                : theme.ramps.yellow(0.5).hex(),
+            inserted: is_light
+                ? theme.ramps.green(0.45).hex()
+                : theme.ramps.green(0.5).hex(),
+            conflict: is_light
+                ? theme.ramps.red(0.6).hex()
+                : theme.ramps.red(0.5).hex(),
+        },
+
         // When two tabs of the same name are open, a label appears next to them
         description: {
             margin: { left: 8 },