Merge branch 'main' into vim-search

Conrad Irwin created

Change summary

Cargo.lock                                       | 368 +++++++++-------
Cargo.toml                                       |   2 
assets/keymaps/vim.json                          |  19 
assets/settings/default.json                     |   7 
crates/client/src/telemetry.rs                   |   2 
crates/editor/src/editor.rs                      |   9 
crates/editor/src/hover_popover.rs               | 116 +++++
crates/editor/src/inlay_hint_cache.rs            | 375 ++++++++++++++--
crates/feedback/src/feedback_editor.rs           |  20 
crates/feedback/src/submit_feedback_button.rs    |  22 
crates/gpui/src/app.rs                           |  12 
crates/gpui/src/app/window.rs                    |  28 
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/language/src/language.rs                  |   1 
crates/lsp/src/lsp.rs                            |   7 
crates/node_runtime/src/node_runtime.rs          |  45 -
crates/project/src/project.rs                    |  11 
crates/terminal/src/terminal.rs                  | 155 +++++-
crates/terminal_view/src/terminal_element.rs     |  26 
crates/terminal_view/src/terminal_panel.rs       |  12 
crates/terminal_view/src/terminal_view.rs        | 132 +++++
crates/theme/src/theme.rs                        |   5 
crates/vector_store/src/embedding.rs             |  12 
crates/vector_store/src/parsing.rs               |   7 
crates/workspace/src/item.rs                     |  46 ++
crates/workspace/src/pane.rs                     | 179 ++++---
crates/workspace/src/workspace.rs                |   1 
crates/zed/Cargo.toml                            |   2 
crates/zed/src/languages.rs                      |   7 
crates/zed/src/languages/bash/brackets.scm       |   3 
crates/zed/src/languages/bash/config.toml        |   8 
crates/zed/src/languages/bash/highlights.scm     |  56 ++
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                           |  21 
crates/zed/src/zed.rs                            |  18 
styles/src/style_tree/feedback.ts                |   5 
styles/src/style_tree/tab_bar.ts                 |  14 
48 files changed, 2,013 insertions(+), 488 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",
@@ -1184,13 +1166,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 +1203,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
 dependencies = [
  "glob",
  "libc",
- "libloading 0.7.4",
+ "libloading",
 ]
 
 [[package]]
@@ -1231,7 +1213,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 +1225,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 +1236,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 +1269,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -1353,11 +1336,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 +1364,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 +1379,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 +1388,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 +1448,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.23",
+ "time 0.3.21",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1557,7 +1550,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 +1641,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 +1653,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 +1686,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 +1736,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 +1863,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 +1880,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 +1891,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 +1914,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.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if 0.1.10",
+ "lazy_static",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.16"
+version = "0.8.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1970,9 +1984,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 +1997,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 +2156,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 +2227,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]]
@@ -2295,7 +2353,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",
@@ -2312,15 +2370,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",
 ]
@@ -2344,7 +2402,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2359,9 +2417,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",
@@ -2484,7 +2542,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.2.16",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2538,7 +2596,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",
@@ -2585,9 +2643,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",
 ]
@@ -2638,7 +2696,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
- "time 0.3.23",
+ "time 0.3.21",
  "util",
 ]
 
@@ -2657,7 +2715,7 @@ dependencies = [
 name = "fsevent"
 version = "2.0.2"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "fsevent-sys",
  "parking_lot 0.11.2",
  "tempdir",
@@ -2684,7 +2742,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",
 ]
 
@@ -2694,12 +2752,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"
@@ -2776,7 +2828,7 @@ dependencies = [
  "futures-io",
  "memchr",
  "parking",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "waker-fn",
 ]
 
@@ -2788,7 +2840,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.25",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -2817,7 +2869,7 @@ dependencies = [
  "futures-sink",
  "futures-task",
  "memchr",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.9",
  "pin-utils",
  "slab",
  "tokio-io",
@@ -2863,9 +2915,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",
@@ -2895,9 +2947,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"
@@ -2925,7 +2977,7 @@ version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags",
  "libc",
  "libgit2-sys",
  "log",
@@ -2940,11 +2992,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "globset"
-version = "0.4.11"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
+checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
 dependencies = [
- "aho-corasick 1.0.2",
+ "aho-corasick 0.7.20",
  "bstr",
  "fnv",
  "log",

Cargo.toml ๐Ÿ”—

@@ -107,6 +107,7 @@ 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" }
@@ -117,6 +118,7 @@ tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex
 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" }

assets/keymaps/vim.json ๐Ÿ”—

@@ -101,6 +101,10 @@
         "vim::SwitchMode",
         "Normal"
       ],
+      "ctrl+[": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "*": "vim::MoveToNext",
       "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
@@ -239,10 +243,6 @@
       "h": "editor::Hover",
       "t": "pane::ActivateNextItem",
       "shift-t": "pane::ActivatePrevItem",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
       "d": "editor::GoToDefinition",
       "shift-d": "editor::GoToTypeDefinition",
       "*": [
@@ -283,10 +283,6 @@
       "t": "editor::ScrollCursorTop",
       "z": "editor::ScrollCursorCenter",
       "b": "editor::ScrollCursorBottom",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ]
     }
   },
   {
@@ -340,7 +336,8 @@
     "context": "Editor && vim_mode == insert",
     "bindings": {
       "escape": "vim::NormalBefore",
-      "ctrl-c": "vim::NormalBefore"
+      "ctrl-c": "vim::NormalBefore",
+      "ctrl-[": "vim::NormalBefore",
     }
   },
   {
@@ -351,6 +348,10 @@
       "escape": [
         "vim::SwitchMode",
         "Normal"
+      ],
+      "ctrl+[": [
+        "vim::SwitchMode",
+        "Normal"
       ]
     }
   },

assets/settings/default.json ๐Ÿ”—

@@ -128,6 +128,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,

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/editor/src/editor.rs ๐Ÿ”—

@@ -2672,11 +2672,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> {

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/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>,

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/gpui/src/app.rs ๐Ÿ”—

@@ -1073,7 +1073,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 +1083,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 +1094,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(
@@ -3399,7 +3399,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,12 +3407,12 @@ 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())
         }
 
         self.keystroke_matcher
-            .bindings_for_action_type(action.as_any().type_id())
+            .bindings_for_action(action.id())
             .find_map(|b| {
                 let highest_handler = handler_depth?;
                 if action.eq(b.action())

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

@@ -14,8 +14,8 @@ use crate::{
     text_layout::TextLayoutCache,
     util::post_inc,
     Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
-    Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
+    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
+    View, ViewContext, ViewHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -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());

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/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/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) {

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/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";
 
@@ -84,9 +84,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 +157,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 +185,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/project/src/project.rs ๐Ÿ”—

@@ -425,6 +425,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 +1915,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 +2717,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")),

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,15 @@ 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! {
+    // Regex Copied from alacritty's ui_config.rs
     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();
+
+    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 +89,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 +506,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 +604,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 +628,7 @@ impl Default for TerminalContent {
             },
             cursor_char: Default::default(),
             size: Default::default(),
-            last_hovered_hyperlink: None,
+            last_hovered_word: None,
         }
     }
 }
@@ -623,7 +645,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 +659,8 @@ pub struct Terminal {
     scroll_px: f32,
     next_link_id: usize,
     selection_phase: SelectionPhase,
+    cmd_pressed: bool,
+    hovered_word: bool,
 }
 
 impl Terminal {
@@ -769,7 +793,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 +828,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 +871,78 @@ 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, 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 = regex_match_at(term, point, &URL_REGEX).is_some();
 
-                    Some((url, url_match))
+                    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 {
@@ -964,6 +1024,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) {
@@ -1035,7 +1104,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(),
         }
     }
 
@@ -1089,14 +1158,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));
@@ -1208,7 +1277,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));
                 }
@@ -1255,8 +1324,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> {
@@ -1334,6 +1403,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 ๐Ÿ”—

@@ -163,6 +163,7 @@ pub struct TerminalElement {
     terminal: WeakModelHandle<Terminal>,
     focused: bool,
     cursor_visible: bool,
+    can_navigate_to_selected_word: bool,
 }
 
 impl TerminalElement {
@@ -170,11 +171,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 +583,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 +626,6 @@ impl Element<TerminalView> for TerminalElement {
             cursor_char,
             selection,
             cursor,
-            last_hovered_hyperlink,
             ..
         } = { &terminal_handle.read(cx).last_content };
 
@@ -634,9 +646,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

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,63 @@ 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 visible = path.path_like.is_dir();
+                        let task_workspace = workspace.clone();
+                        cx.spawn(|_, mut cx| async move {
+                            let opened_item = task_workspace
+                                .update(&mut cx, |workspace, cx| {
+                                    workspace.open_abs_path(path.path_like, visible, cx)
+                                })
+                                .context("workspace update")?
+                                .await
+                                .context("workspace update")?;
+                            if let Some(row) = path.row {
+                                let col = path.column.unwrap_or(0);
+                                if let Some(active_editor) = opened_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 +239,7 @@ impl TerminalView {
             blinking_on: false,
             blinking_paused: false,
             blink_epoch: 0,
+            can_navigate_to_selected_word: false,
             workspace_id,
         }
     }
@@ -344,6 +413,40 @@ 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 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 +475,7 @@ impl View for TerminalView {
                     terminal_handle,
                     focused,
                     self.should_show_cursor(focused, cx),
+                    self.can_navigate_to_selected_word,
                 )
                 .contained(),
             )
@@ -393,6 +497,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 +736,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))
             })?)
         })
     }

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)]
@@ -722,12 +723,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/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/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/workspace/src/item.rs ๐Ÿ”—

@@ -10,6 +10,9 @@ use gpui::{
     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 +30,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,

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,
@@ -866,6 +868,7 @@ impl Pane {
                 .paths_by_item
                 .get(&item.id())
                 .and_then(|(_, abs_path)| abs_path.clone());
+
             self.nav_history
                 .0
                 .borrow_mut()
@@ -1157,6 +1160,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 +1182,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 +1208,7 @@ impl Pane {
                                         ix == 0,
                                         detail,
                                         hovered,
-                                        tab_style,
+                                        &tab_style,
                                         cx,
                                     )
                                 })
@@ -1350,81 +1370,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<

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

@@ -203,6 +203,7 @@ 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) {

crates/zed/Cargo.toml ๐Ÿ”—

@@ -104,6 +104,7 @@ thiserror.workspace = true
 tiny_http = "0.8"
 toml.workspace = true
 tree-sitter.workspace = true
+tree-sitter-bash.workspace = true
 tree-sitter-c.workspace = true
 tree-sitter-cpp.workspace = true
 tree-sitter-css.workspace = true
@@ -119,6 +120,7 @@ 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

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,11 @@ 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))],
+    );
 }
 
 #[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,56 @@
+[
+  (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"
+] @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/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,
@@ -405,11 +405,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 +471,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 +487,8 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
                 }
             }
         }
+
+        std::process::abort();
     }));
 }
 
@@ -887,7 +895,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/zed.rs ๐Ÿ”—

@@ -517,11 +517,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 +526,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 +537,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,

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/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 },