brought up to speed with main

KCaverly created

Change summary

.github/workflows/release_actions.yml                   |   4 
Cargo.lock                                              | 212 ++++---
Cargo.toml                                              |   1 
assets/icons/radix/maximize.svg                         |   4 
assets/icons/radix/minimize.svg                         |   4 
assets/keymaps/default.json                             |   1 
assets/keymaps/textmate.json                            |   1 
assets/keymaps/vim.json                                 |  18 
crates/ai/src/assistant.rs                              |   2 
crates/call/src/call.rs                                 |  43 +
crates/cli/src/main.rs                                  |   1 
crates/client/src/telemetry.rs                          |   4 
crates/collab/src/tests/integration_tests.rs            |   6 
crates/collab/src/tests/randomized_integration_tests.rs |   2 
crates/collab_ui/Cargo.toml                             |   1 
crates/collab_ui/src/collab_titlebar_item.rs            |  27 
crates/collab_ui/src/collab_ui.rs                       |  18 
crates/collab_ui/src/incoming_call_notification.rs      |   4 
crates/db/src/db.rs                                     |   3 
crates/editor/src/editor.rs                             |  59 ++
crates/editor/src/editor_tests.rs                       | 160 +++++
crates/editor/src/element.rs                            |  19 
crates/editor/src/inlay_hint_cache.rs                   |  10 
crates/editor/src/items.rs                              |   2 
crates/editor/src/movement.rs                           |  24 
crates/editor/src/test/editor_test_context.rs           |  26 
crates/language/src/language.rs                         |  34 +
crates/project/src/project.rs                           |  17 
crates/search/src/project_search.rs                     |   3 
crates/theme/src/theme.rs                               |   1 
crates/vcs_menu/Cargo.toml                              |  16 
crates/vcs_menu/src/lib.rs                              |  36 +
crates/vim/src/motion.rs                                | 160 ++++++
crates/vim/src/normal/case.rs                           |  96 ++-
crates/vim/src/test.rs                                  |  14 
crates/vim/src/test/neovim_backed_test_context.rs       |  51 +
crates/vim/src/test/neovim_connection.rs                |  16 
crates/vim/src/test/vim_test_context.rs                 |   2 
crates/vim/src/vim.rs                                   |  23 
crates/vim/test_data/test_change_case.json              |  18 
crates/vim/test_data/test_matching.json                 |  17 
crates/vim/test_data/test_start_end_of_paragraph.json   |  13 
crates/zed/src/main.rs                                  |   9 
crates/zed/src/only_instance.rs                         | 103 +++
crates/zed/src/zed.rs                                   |   1 
styles/src/component/tab_bar_button.ts                  |  55 ++
styles/src/style_tree/assistant.ts                      | 287 +++-------
styles/src/theme/create_theme.ts                        |   9 
48 files changed, 1,178 insertions(+), 459 deletions(-)

Detailed changes

.github/workflows/release_actions.yml 🔗

@@ -16,8 +16,4 @@ jobs:
 
           Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
 
-          ```md
-          # Changelog
-
           ${{ github.event.release.body }}
-          ```

Cargo.lock 🔗

@@ -318,9 +318,9 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
 dependencies = [
  "concurrent-queue",
  "event-listener",
@@ -374,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
 dependencies = [
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "futures-lite",
 ]
@@ -401,7 +401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
 dependencies = [
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-lite",
@@ -430,7 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
 dependencies = [
  "async-io",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "futures-lite",
 ]
@@ -452,7 +452,7 @@ checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
 dependencies = [
  "async-io",
  "async-lock",
- "autocfg 1.1.0",
+ "autocfg",
  "blocking",
  "cfg-if 1.0.0",
  "event-listener",
@@ -481,7 +481,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -529,7 +529,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -572,7 +572,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -658,15 +658,6 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "autocfg"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
-dependencies = [
- "autocfg 1.1.0",
-]
-
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -839,7 +830,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.23",
+ "syn 2.0.25",
  "which",
 ]
 
@@ -1001,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
 dependencies = [
  "memchr",
- "regex-automata 0.3.1",
+ "regex-automata 0.3.2",
  "serde",
 ]
 
@@ -1295,7 +1286,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -1362,7 +1353,7 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.22",
+ "time 0.3.23",
  "tiny_http",
  "url",
  "util",
@@ -1464,7 +1455,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.22",
+ "time 0.3.23",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1506,6 +1497,7 @@ dependencies = [
  "theme",
  "theme_selector",
  "util",
+ "vcs_menu",
  "workspace",
  "zed-actions",
 ]
@@ -1905,7 +1897,7 @@ version = "0.9.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "cfg-if 1.0.0",
  "crossbeam-utils",
  "memoffset 0.9.0",
@@ -1994,12 +1986,12 @@ dependencies = [
 
 [[package]]
 name = "dashmap"
-version = "5.4.0"
+version = "5.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
+checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
 dependencies = [
  "cfg-if 1.0.0",
- "hashbrown 0.12.3",
+ "hashbrown 0.14.0",
  "lock_api",
  "once_cell",
  "parking_lot_core 0.9.8",
@@ -2322,9 +2314,9 @@ dependencies = [
 
 [[package]]
 name = "equivalent"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erased-serde"
@@ -2648,7 +2640,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
- "time 0.3.22",
+ "time 0.3.23",
  "util",
 ]
 
@@ -2798,7 +2790,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -3038,7 +3030,7 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
- "time 0.3.22",
+ "time 0.3.23",
  "tiny-skia",
  "usvg",
  "util",
@@ -3412,7 +3404,7 @@ version = "1.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "hashbrown 0.12.3",
  "serde",
 ]
@@ -3537,7 +3529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
 dependencies = [
  "hermit-abi 0.3.2",
- "rustix 0.38.3",
+ "rustix 0.38.4",
  "windows-sys",
 ]
 
@@ -4023,7 +4015,7 @@ version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "scopeguard",
 ]
 
@@ -4127,7 +4119,7 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "rawpointer",
 ]
 
@@ -4190,7 +4182,7 @@ version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
 ]
 
 [[package]]
@@ -4199,7 +4191,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
 ]
 
 [[package]]
@@ -4251,7 +4243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
 dependencies = [
  "adler",
- "autocfg 1.1.0",
+ "autocfg",
 ]
 
 [[package]]
@@ -4513,18 +4505,17 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "num-integer",
  "num-traits",
 ]
 
 [[package]]
 name = "num-bigint-dig"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480"
+checksum = "f9bc3e36fd683e004fd59c64a425e0e991616f5a8b617c3b9a933a93c168facc"
 dependencies = [
- "autocfg 0.1.8",
  "byteorder",
  "lazy_static",
  "libm",
@@ -4553,7 +4544,7 @@ version = "0.1.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "num-traits",
 ]
 
@@ -4563,7 +4554,7 @@ version = "0.1.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "num-integer",
  "num-traits",
 ]
@@ -4574,7 +4565,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "num-integer",
  "num-traits",
 ]
@@ -4585,7 +4576,7 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "libm",
 ]
 
@@ -4742,7 +4733,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -5030,7 +5021,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -5059,16 +5050,16 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
 [[package]]
 name = "plist"
-version = "1.4.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
+checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
 dependencies = [
  "base64 0.21.2",
  "indexmap 1.9.3",
  "line-wrap",
  "quick-xml",
  "serde",
- "time 0.3.22",
+ "time 0.3.23",
 ]
 
 [[package]]
@@ -5127,7 +5118,7 @@ version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "bitflags 1.3.2",
  "cfg-if 1.0.0",
  "concurrent-queue",
@@ -5183,7 +5174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
 dependencies = [
  "proc-macro2",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -5231,9 +5222,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.63"
+version = "1.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
 dependencies = [
  "unicode-ident",
 ]
@@ -5502,9 +5493,9 @@ dependencies = [
 
 [[package]]
 name = "quick-xml"
-version = "0.28.2"
+version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
+checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
 dependencies = [
  "memchr",
 ]
@@ -5742,8 +5733,8 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
 dependencies = [
  "aho-corasick 1.0.2",
  "memchr",
- "regex-automata 0.3.1",
- "regex-syntax 0.7.3",
+ "regex-automata 0.3.2",
+ "regex-syntax 0.7.4",
 ]
 
 [[package]]
@@ -5757,13 +5748,13 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9aaecc05d5c4b5f7da074b9a0d1a0867e71fd36e7fc0482d8bcfe8e8fc56290"
+checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
 dependencies = [
  "aho-corasick 1.0.2",
  "memchr",
- "regex-syntax 0.7.3",
+ "regex-syntax 0.7.4",
 ]
 
 [[package]]
@@ -5774,9 +5765,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "regex-syntax"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
+checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
 
 [[package]]
 name = "region"
@@ -6054,7 +6045,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn 2.0.23",
+ "syn 2.0.25",
  "walkdir",
 ]
 
@@ -6140,9 +6131,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.3"
+version = "0.38.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
+checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
 dependencies = [
  "bitflags 2.3.3",
  "errno 0.3.1",
@@ -6346,7 +6337,7 @@ dependencies = [
  "serde_json",
  "sqlx",
  "thiserror",
- "time 0.3.22",
+ "time 0.3.23",
  "tracing",
  "url",
  "uuid 1.4.0",
@@ -6374,7 +6365,7 @@ dependencies = [
  "rust_decimal",
  "sea-query-derive",
  "serde_json",
- "time 0.3.22",
+ "time 0.3.23",
  "uuid 1.4.0",
 ]
 
@@ -6389,7 +6380,7 @@ dependencies = [
  "sea-query",
  "serde_json",
  "sqlx",
- "time 0.3.22",
+ "time 0.3.23",
  "uuid 1.4.0",
 ]
 
@@ -6511,22 +6502,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
 
 [[package]]
 name = "serde"
-version = "1.0.167"
+version = "1.0.171"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237"
+checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.167"
+version = "1.0.171"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9"
+checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -6581,7 +6572,7 @@ checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -6807,7 +6798,7 @@ version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
 ]
 
 [[package]]
@@ -7000,7 +6991,7 @@ dependencies = [
  "sqlx-rt",
  "stringprep",
  "thiserror",
- "time 0.3.22",
+ "time 0.3.23",
  "tokio-stream",
  "url",
  "uuid 1.4.0",
@@ -7249,9 +7240,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.23"
+version = "2.0.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
+checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7319,9 +7310,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.8"
+version = "0.12.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac"
+checksum = "df8e77cb757a61f51b947ec4a7e3646efd825b73561db1c232a8ccb639e611a0"
 
 [[package]]
 name = "tempdir"
@@ -7339,7 +7330,7 @@ version = "3.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "cfg-if 1.0.0",
  "fastrand",
  "redox_syscall 0.3.5",
@@ -7505,7 +7496,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -7578,9 +7569,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.22"
+version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
 dependencies = [
  "itoa 1.0.8",
  "serde",
@@ -7596,9 +7587,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
 
 [[package]]
 name = "time-macros"
-version = "0.2.9"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
 dependencies = [
  "time-core",
 ]
@@ -7651,7 +7642,7 @@ version = "1.29.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
 dependencies = [
- "autocfg 1.1.0",
+ "autocfg",
  "backtrace",
  "bytes 1.4.0",
  "libc",
@@ -7694,7 +7685,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -7899,7 +7890,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]
@@ -8483,6 +8474,19 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
+[[package]]
+name = "vcs_menu"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fuzzy",
+ "gpui",
+ "picker",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "vector_store"
 version = "0.1.0"
@@ -8678,7 +8682,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
  "wasm-bindgen-shared",
 ]
 
@@ -8712,7 +8716,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -8725,9 +8729,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
 [[package]]
 name = "wasm-encoder"
-version = "0.29.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18c41dbd92eaebf3612a39be316540b8377c871cb9bde6b064af962984912881"
+checksum = "b2f8e9778e04cbf44f58acc301372577375a666b966c50b03ef46144f80436a8"
 dependencies = [
  "leb128",
 ]
@@ -8949,9 +8953,9 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "60.0.0"
+version = "61.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd06cc744b536e30387e72a48fdd492105b9c938bb4f415c39c616a7a0a697ad"
+checksum = "dc6b347851b52fd500657d301155c79e8c67595501d179cef87b6f04ebd25ac4"
 dependencies = [
  "leb128",
  "memchr",
@@ -8961,11 +8965,11 @@ dependencies = [
 
 [[package]]
 name = "wat"
-version = "1.0.66"
+version = "1.0.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5abe520f0ab205366e9ac7d3e6b2fc71de44e32a2b58f2ec871b6b575bdcea3b"
+checksum = "459e764d27c3ab7beba1ebd617cc025c7e76dea6e7c5ce3189989a970aea3491"
 dependencies = [
- "wast 60.0.0",
+ "wast 61.0.0",
 ]
 
 [[package]]
@@ -9295,9 +9299,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
 [[package]]
 name = "winnow"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9482fe6ceabdf32f3966bfdd350ba69256a97c30253dc616fe0005af24f164e"
+checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529"
 dependencies = [
  "memchr",
 ]
@@ -9601,7 +9605,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.25",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -65,6 +65,7 @@ members = [
     "crates/util",
     "crates/vector_store",
     "crates/vim",
+    "crates/vcs_menu",
     "crates/workspace",
     "crates/welcome",
     "crates/xtask",

assets/icons/radix/maximize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
+<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
+</svg>

assets/icons/radix/minimize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
+<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
+</svg>

assets/keymaps/default.json 🔗

@@ -39,6 +39,7 @@
       "cmd-shift-n": "workspace::NewWindow",
       "cmd-o": "workspace::Open",
       "alt-cmd-o": "projects::OpenRecent",
+      "alt-cmd-b": "branches::OpenRecent",
       "ctrl-~": "workspace::NewTerminal",
       "ctrl-`": "terminal_panel::ToggleFocus",
       "shift-escape": "workspace::ToggleZoom"

assets/keymaps/textmate.json 🔗

@@ -2,6 +2,7 @@
   {
     "bindings": {
       "cmd-shift-o": "projects::OpenRecent",
+      "cmd-shift-b": "branches::OpenRecent",
       "cmd-alt-tab": "project_panel::ToggleFocus"
     }
   },

assets/keymaps/vim.json 🔗

@@ -35,8 +35,11 @@
       "l": "vim::Right",
       "right": "vim::Right",
       "$": "vim::EndOfLine",
+      "^": "vim::FirstNonWhitespace",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
+      "{": "vim::StartOfParagraph",
+      "}": "vim::EndOfParagraph",
       "shift-w": [
         "vim::NextWordStart",
         {
@@ -92,7 +95,10 @@
       ],
       "ctrl-o": "pane::GoBack",
       "ctrl-]": "editor::GoToDefinition",
-      "escape": "editor::Cancel",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
       "1": [
         "vim::Number",
@@ -165,7 +171,6 @@
       "shift-a": "vim::InsertEndOfLine",
       "x": "vim::DeleteRight",
       "shift-x": "vim::DeleteLeft",
-      "^": "vim::FirstNonWhitespace",
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
       "~": "vim::ChangeCase",
@@ -305,6 +310,10 @@
         "vim::PushOperator",
         "Replace"
       ],
+      "ctrl-c": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "> >": "editor::Indent",
       "< <": "editor::Outdent"
     }
@@ -321,7 +330,10 @@
     "bindings": {
       "tab": "vim::Tab",
       "enter": "vim::Enter",
-      "escape": "editor::Cancel"
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ]
     }
   }
 ]

crates/ai/src/assistant.rs 🔗

@@ -2061,6 +2061,8 @@ impl ConversationEditor {
         let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
         let remaining_tokens_style = if remaining_tokens <= 0 {
             &style.no_remaining_tokens
+        } else if remaining_tokens <= 500 {
+            &style.low_remaining_tokens
         } else {
             &style.remaining_tokens
         };

crates/call/src/call.rs 🔗

@@ -4,7 +4,7 @@ pub mod room;
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
-use client::{proto, Client, TypedEnvelope, User, UserStore};
+use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
 use postage::watch;
@@ -198,6 +198,7 @@ impl ActiveCall {
             let result = invite.await;
             this.update(&mut cx, |this, cx| {
                 this.pending_invites.remove(&called_user_id);
+                this.report_call_event("invite", cx);
                 cx.notify();
             });
             result
@@ -243,21 +244,26 @@ impl ActiveCall {
         };
 
         let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
+
         cx.spawn(|this, mut cx| async move {
             let room = join.await?;
             this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
                 .await?;
+            this.update(&mut cx, |this, cx| {
+                this.report_call_event("accept incoming", cx)
+            });
             Ok(())
         })
     }
 
-    pub fn decline_incoming(&mut self) -> Result<()> {
+    pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
         let call = self
             .incoming_call
             .0
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
+        self.report_call_event_for_room("decline incoming", call.room_id, cx);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -266,6 +272,7 @@ impl ActiveCall {
 
     pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         cx.notify();
+        self.report_call_event("hang up", cx);
         if let Some((room, _)) = self.room.take() {
             room.update(cx, |room, cx| room.leave(cx))
         } else {
@@ -273,12 +280,28 @@ impl ActiveCall {
         }
     }
 
+    pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
+        if let Some(room) = self.room().cloned() {
+            let toggle_screen_sharing = room.update(cx, |room, cx| {
+                if room.is_screen_sharing() {
+                    self.report_call_event("disable screen share", cx);
+                    Task::ready(room.unshare_screen(cx))
+                } else {
+                    self.report_call_event("enable screen share", cx);
+                    room.share_screen(cx)
+                }
+            });
+            toggle_screen_sharing.detach_and_log_err(cx);
+        }
+    }
+
     pub fn share_project(
         &mut self,
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<u64>> {
         if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("share project", cx);
             room.update(cx, |room, cx| room.share_project(project, cx))
         } else {
             Task::ready(Err(anyhow!("no active call")))
@@ -291,6 +314,7 @@ impl ActiveCall {
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("unshare project", cx);
             room.update(cx, |room, cx| room.unshare_project(project, cx))
         } else {
             Err(anyhow!("no active call"))
@@ -352,4 +376,19 @@ impl ActiveCall {
     pub fn pending_invites(&self) -> &HashSet<u64> {
         &self.pending_invites
     }
+
+    fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
+        if let Some(room) = self.room() {
+            self.report_call_event_for_room(operation, room.read(cx).id(), cx)
+        }
+    }
+
+    fn report_call_event_for_room(&self, operation: &'static str, room_id: u64, cx: &AppContext) {
+        let telemetry = self.client.telemetry();
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+        let event = ClickhouseEvent::Call { operation, room_id };
+
+        telemetry.report_clickhouse_event(event, telemetry_settings);
+    }
 }

crates/cli/src/main.rs 🔗

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

crates/client/src/telemetry.rs 🔗

@@ -70,6 +70,10 @@ pub enum ClickhouseEvent {
         suggestion_accepted: bool,
         file_extension: Option<String>,
     },
+    Call {
+        operation: &'static str,
+        room_id: u64,
+    },
 }
 
 #[cfg(debug_assertions)]

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

@@ -157,7 +157,7 @@ async fn test_basic_calls(
     // User C receives the call, but declines it.
     let call_c = incoming_call_c.next().await.unwrap().unwrap();
     assert_eq!(call_c.calling_user.github_login, "user_b");
-    active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
+    active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
     assert!(incoming_call_c.next().await.unwrap().is_none());
 
     deterministic.run_until_parked();
@@ -1080,7 +1080,7 @@ async fn test_calls_on_multiple_connections(
 
     // User B declines the call on one of the two connections, causing both connections
     // to stop ringing.
-    active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
+    active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
     deterministic.run_until_parked();
     assert!(incoming_call_b1.next().await.unwrap().is_none());
     assert!(incoming_call_b2.next().await.unwrap().is_none());
@@ -5945,7 +5945,7 @@ async fn test_contacts(
         [("user_b".to_string(), "online", "busy")]
     );
 
-    active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
+    active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
     deterministic.run_until_parked();
     assert_eq!(
         contacts(&client_a, cx_a),

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

@@ -365,7 +365,7 @@ async fn apply_client_operation(
             }
 
             log::info!("{}: declining incoming call", client.username);
-            active_call.update(cx, |call, _| call.decline_incoming())?;
+            active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
         }
 
         ClientOperation::LeaveCall => {

crates/collab_ui/Cargo.toml 🔗

@@ -39,6 +39,7 @@ recent_projects = {path = "../recent_projects"}
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
+vcs_menu = { path = "../vcs_menu" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 zed-actions = {path = "../zed-actions"}

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,8 +1,5 @@
 use crate::{
-    branch_list::{build_branch_list, BranchList},
-    contact_notification::ContactNotification,
-    contacts_popover,
-    face_pile::FacePile,
+    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
     toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
     ToggleScreenSharing,
 };
@@ -27,6 +24,7 @@ use recent_projects::{build_recent_projects, RecentProjects};
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
+use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
 use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -37,7 +35,6 @@ actions!(
     [
         ToggleContactsMenu,
         ToggleUserMenu,
-        ToggleVcsMenu,
         ToggleProjectMenu,
         SwitchBranch,
         ShareProject,
@@ -229,15 +226,23 @@ impl CollabTitlebarItem {
         let mut ret = Flex::row().with_child(
             Stack::new()
                 .with_child(
-                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, cx| {
                         let style = project_style
                             .in_state(self.project_popover.is_some())
                             .style_for(mouse_state);
+                        enum RecentProjectsTooltip {}
                         Label::new(name, style.text.clone())
                             .contained()
                             .with_style(style.container)
                             .aligned()
                             .left()
+                            .with_tooltip::<RecentProjectsTooltip>(
+                                0,
+                                "Recent projects".into(),
+                                Some(Box::new(recent_projects::OpenRecent)),
+                                theme.tooltip.clone(),
+                                cx,
+                            )
                             .into_any_named("title-project-name")
                     })
                     .with_cursor_style(CursorStyle::PointingHand)
@@ -264,7 +269,8 @@ impl CollabTitlebarItem {
                                 MouseEventHandler::<ToggleVcsMenu, Self>::new(
                                     0,
                                     cx,
-                                    |mouse_state, _| {
+                                    |mouse_state, cx| {
+                                        enum BranchPopoverTooltip {}
                                         let style = git_style
                                             .in_state(self.branch_popover.is_some())
                                             .style_for(mouse_state);
@@ -274,6 +280,13 @@ impl CollabTitlebarItem {
                                             .with_margin_right(item_spacing)
                                             .aligned()
                                             .left()
+                                            .with_tooltip::<BranchPopoverTooltip>(
+                                                0,
+                                                "Recent branches".into(),
+                                                Some(Box::new(ToggleVcsMenu)),
+                                                theme.tooltip.clone(),
+                                                cx,
+                                            )
                                             .into_any_named("title-project-branch")
                                     },
                                 )

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,4 +1,3 @@
-mod branch_list;
 mod collab_titlebar_item;
 mod contact_finder;
 mod contact_list;
@@ -12,7 +11,7 @@ mod sharing_status_indicator;
 
 use call::{ActiveCall, Room};
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
-use gpui::{actions, AppContext, Task};
+use gpui::{actions, AppContext};
 use std::sync::Arc;
 use util::ResultExt;
 use workspace::AppState;
@@ -29,7 +28,7 @@ actions!(
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    branch_list::init(cx);
+    vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);
@@ -45,16 +44,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        let toggle_screen_sharing = room.update(cx, |room, cx| {
-            if room.is_screen_sharing() {
-                Task::ready(room.unshare_screen(cx))
-            } else {
-                room.share_screen(cx)
-            }
-        });
-        toggle_screen_sharing.detach_and_log_err(cx);
-    }
+    ActiveCall::global(cx).update(cx, |call, cx| {
+        call.toggle_screen_sharing(cx);
+    });
 }
 
 pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -99,8 +99,8 @@ impl IncomingCallNotification {
                 })
                 .detach_and_log_err(cx);
         } else {
-            active_call.update(cx, |active_call, _| {
-                active_call.decline_incoming().log_err();
+            active_call.update(cx, |active_call, cx| {
+                active_call.decline_incoming(cx).log_err();
             });
         }
     }

crates/db/src/db.rs 🔗

@@ -41,8 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
 const DB_FILE_NAME: &'static str = "db.sqlite";
 
 lazy_static::lazy_static! {
-    // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
-    static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
+    pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
     pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
     pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
 }

crates/editor/src/editor.rs 🔗

@@ -5123,7 +5123,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::start_of_paragraph(map, selection.head()),
+                    movement::start_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5143,7 +5143,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::end_of_paragraph(map, selection.head()),
+                    movement::end_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5162,7 +5162,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::start_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::start_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }
@@ -5179,7 +5182,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::end_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::end_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }
@@ -7216,6 +7222,47 @@ impl Editor {
         }
         results
     }
+    pub fn background_highlights_in_range_for<T: 'static>(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        theme: &Theme,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        let Some((color_fetcher, ranges)) = self.background_highlights
+            .get(&TypeId::of::<T>()) else {
+                return vec![];
+            };
+
+        let color = color_fetcher(theme);
+        let start_ix = match ranges.binary_search_by(|probe| {
+            let cmp = probe.end.cmp(&search_range.start, buffer);
+            if cmp.is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Less
+            }
+        }) {
+            Ok(i) | Err(i) => i,
+        };
+        for range in &ranges[start_ix..] {
+            if range.start.cmp(&search_range.end, buffer).is_ge() {
+                break;
+            }
+            let start = range
+                .start
+                .to_point(buffer)
+                .to_display_point(display_snapshot);
+            let end = range
+                .end
+                .to_point(buffer)
+                .to_display_point(display_snapshot);
+            results.push((start..end, color))
+        }
+
+        results
+    }
 
     pub fn highlight_text<T: 'static>(
         &mut self,
@@ -7518,7 +7565,7 @@ impl Editor {
 
     fn report_editor_event(
         &self,
-        name: &'static str,
+        operation: &'static str,
         file_extension: Option<String>,
         cx: &AppContext,
     ) {
@@ -7555,7 +7602,7 @@ impl Editor {
         let event = ClickhouseEvent::Editor {
             file_extension,
             vim_mode,
-            operation: name,
+            operation,
             copilot_enabled,
             copilot_enabled_for_language,
         };

crates/editor/src/editor_tests.rs 🔗

@@ -22,7 +22,10 @@ use language::{
     BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
 };
 use parking_lot::Mutex;
+use project::project_settings::{LspSettings, ProjectSettings};
 use project::FakeFs;
+use std::sync::atomic;
+use std::sync::atomic::AtomicUsize;
 use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
 use unindent::Unindent;
 use util::{
@@ -1796,7 +1799,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
     "});
     }
     // Ensure that comment continuations can be disabled.
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.defaults.extend_comment_on_newline = Some(false);
     });
     let mut cx = EditorTestContext::new(cx).await;
@@ -4546,7 +4549,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overridden tabsize is sent to language server
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.languages.insert(
             "Rust".into(),
             LanguageSettingsContent {
@@ -4660,7 +4663,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overridden tabsize is sent to language server
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.languages.insert(
             "Rust".into(),
             LanguageSettingsContent {
@@ -7084,6 +7087,142 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language_name: Arc<str> = "Rust".into();
+    let mut language = Language::new(
+        LanguageConfig {
+            name: Arc::clone(&language_name),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+
+    let server_restarts = Arc::new(AtomicUsize::new(0));
+    let closure_restarts = Arc::clone(&server_restarts);
+    let language_server_name = "test language server";
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: language_server_name,
+            initialization_options: Some(json!({
+                "testOptionValue": true
+            })),
+            initializer: Some(Box::new(move |fake_server| {
+                let task_restarts = Arc::clone(&closure_restarts);
+                fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
+                    task_restarts.fetch_add(1, atomic::Ordering::Release);
+                    futures::future::ready(Ok(()))
+                });
+            })),
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let _buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    let _fake_server = fake_servers.next().await.unwrap();
+    update_test_language_settings(cx, |language_settings| {
+        language_settings.languages.insert(
+            Arc::clone(&language_name),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            "Some other server name".into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "some other init value": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should restart LSP server on a related LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should not restart LSP server on a related LSP settings change that is the same"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: None,
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        2,
+        "Should restart LSP server on another related LSP settings change"
+    );
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point
@@ -7203,7 +7342,7 @@ fn handle_copilot_completion_request(
     });
 }
 
-pub(crate) fn update_test_settings(
+pub(crate) fn update_test_language_settings(
     cx: &mut TestAppContext,
     f: impl Fn(&mut AllLanguageSettingsContent),
 ) {
@@ -7214,6 +7353,17 @@ pub(crate) fn update_test_settings(
     });
 }
 
+pub(crate) fn update_test_project_settings(
+    cx: &mut TestAppContext,
+    f: impl Fn(&mut ProjectSettings),
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<ProjectSettings>(cx, f);
+        });
+    });
+}
+
 pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
     cx.foreground().forbid_parking();
 
@@ -7227,5 +7377,5 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
         crate::init(cx);
     });
 
-    update_test_settings(cx, f);
+    update_test_language_settings(cx, f);
 }

crates/editor/src/element.rs 🔗

@@ -1086,11 +1086,13 @@ impl EditorElement {
                         })
                     }
                 };
-                for (row, _) in &editor.background_highlights_in_range(
-                    start_anchor..end_anchor,
-                    &layout.position_map.snapshot,
-                    &theme,
-                ) {
+                for (row, _) in &editor
+                    .background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
+                        start_anchor..end_anchor,
+                        &layout.position_map.snapshot,
+                        &theme,
+                    )
+                {
                     let start_display = row.start;
                     let end_display = row.end;
 
@@ -2149,6 +2151,9 @@ impl Element<Editor> for EditorElement {
             ShowScrollbar::Auto => {
                 // Git
                 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+                ||
+                // Selections
+                (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
                 // Scrollmanager
                 || editor.scroll_manager.scrollbars_visible()
             }
@@ -2911,7 +2916,7 @@ mod tests {
     use super::*;
     use crate::{
         display_map::{BlockDisposition, BlockProperties},
-        editor_tests::{init_test, update_test_settings},
+        editor_tests::{init_test, update_test_language_settings},
         Editor, MultiBuffer,
     };
     use gpui::TestAppContext;
@@ -3108,7 +3113,7 @@ mod tests {
         let resize_step = 10.0;
         let mut editor_width = 200.0;
         while editor_width <= 1000.0 {
-            update_test_settings(cx, |s| {
+            update_test_language_settings(cx, |s| {
                 s.defaults.tab_size = NonZeroU32::new(tab_size);
                 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
                 s.defaults.preferred_line_length = Some(editor_width as u32);

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -847,7 +847,7 @@ mod tests {
     use text::Point;
     use workspace::Workspace;
 
-    use crate::editor_tests::update_test_settings;
+    use crate::editor_tests::update_test_language_settings;
 
     use super::*;
 
@@ -1476,7 +1476,7 @@ mod tests {
             ),
         ] {
             edits_made += 1;
-            update_test_settings(cx, |settings| {
+            update_test_language_settings(cx, |settings| {
                 settings.defaults.inlay_hints = Some(InlayHintSettings {
                     enabled: true,
                     show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1520,7 +1520,7 @@ mod tests {
 
         edits_made += 1;
         let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
-        update_test_settings(cx, |settings| {
+        update_test_language_settings(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: false,
                 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1577,7 +1577,7 @@ mod tests {
 
         let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
         edits_made += 1;
-        update_test_settings(cx, |settings| {
+        update_test_language_settings(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: true,
                 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -2269,7 +2269,7 @@ unedited (2nd) buffer should have the same hint");
             crate::init(cx);
         });
 
-        update_test_settings(cx, f);
+        update_test_language_settings(cx, f);
     }
 
     async fn prepare_test_objects(

crates/editor/src/items.rs 🔗

@@ -883,7 +883,7 @@ impl ProjectItem for Editor {
     }
 }
 
-enum BufferSearchHighlights {}
+pub(crate) enum BufferSearchHighlights {}
 impl SearchableItem for Editor {
     type Match = Range<Anchor>;
 

crates/editor/src/movement.rs 🔗

@@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
     })
 }
 
-pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn start_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
     let point = display_point.to_point(map);
     if point.row == 0 {
         return map.max_point();
@@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     for row in (0..point.row + 1).rev() {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;
@@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     DisplayPoint::zero()
 }
 
-pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn end_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
     let point = display_point.to_point(map);
     if point.row == map.max_buffer_row() {
         return DisplayPoint::zero();
@@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
     for row in point.row..map.max_buffer_row() + 1 {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;

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

@@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> {
         self.assert_selections(expected_selections, marked_text.to_string())
     }
 
+    pub fn editor_state(&mut self) -> String {
+        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+    }
+
     #[track_caller]
     pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
         let expected_ranges = self.ranges(marked_text);
@@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> {
         self.assert_selections(expected_selections, expected_marked_text)
     }
 
-    #[track_caller]
-    fn assert_selections(
-        &mut self,
-        expected_selections: Vec<Range<usize>>,
-        expected_marked_text: String,
-    ) {
-        let actual_selections = self
-            .editor
+    fn editor_selections(&self) -> Vec<Range<usize>> {
+        self.editor
             .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
             .into_iter()
             .map(|s| {
@@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> {
                     s.start..s.end
                 }
             })
-            .collect::<Vec<_>>();
+            .collect::<Vec<_>>()
+    }
+
+    #[track_caller]
+    fn assert_selections(
+        &mut self,
+        expected_selections: Vec<Range<usize>>,
+        expected_marked_text: String,
+    ) {
+        let actual_selections = self.editor_selections();
         let actual_marked_text =
             generate_marked_text(&self.buffer_text(), &actual_selections, true);
         if expected_selections != actual_selections {
             panic!(
                 indoc! {"
+
                     {}Editor has unexpected selections.
 
                     Expected selections:

crates/language/src/language.rs 🔗

@@ -90,7 +90,8 @@ pub struct LanguageServerName(pub Arc<str>);
 /// once at startup, and caches the results.
 pub struct CachedLspAdapter {
     pub name: LanguageServerName,
-    pub initialization_options: Option<Value>,
+    initialization_options: Option<Value>,
+    initialization_overrides: Mutex<Option<Value>>,
     pub disk_based_diagnostic_sources: Vec<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub language_ids: HashMap<String, String>,
@@ -109,6 +110,7 @@ impl CachedLspAdapter {
         Arc::new(CachedLspAdapter {
             name,
             initialization_options,
+            initialization_overrides: Mutex::new(None),
             disk_based_diagnostic_sources,
             disk_based_diagnostics_progress_token,
             language_ids,
@@ -208,6 +210,30 @@ impl CachedLspAdapter {
     ) -> Option<CodeLabel> {
         self.adapter.label_for_symbol(name, kind, language).await
     }
+
+    pub fn update_initialization_overrides(&self, new: Option<&Value>) -> bool {
+        let mut current = self.initialization_overrides.lock();
+        if current.as_ref() != new {
+            *current = new.cloned();
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn initialization_options(&self) -> Option<Value> {
+        let initialization_options = self.initialization_options.as_ref();
+        let override_options = self.initialization_overrides.lock().clone();
+        match (initialization_options, override_options) {
+            (None, override_options) => override_options,
+            (initialization_options, None) => initialization_options.cloned(),
+            (Some(initialization_options), Some(override_options)) => {
+                let mut initialization_options = initialization_options.clone();
+                merge_json_value_into(override_options, &mut initialization_options);
+                Some(initialization_options)
+            }
+        }
+    }
 }
 
 pub trait LspAdapterDelegate: Send + Sync {
@@ -428,6 +454,7 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
 #[cfg(any(test, feature = "test-support"))]
 pub struct FakeLspAdapter {
     pub name: &'static str,
+    pub initialization_options: Option<Value>,
     pub capabilities: lsp::ServerCapabilities,
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
@@ -1681,6 +1708,7 @@ impl Default for FakeLspAdapter {
             capabilities: lsp::LanguageServer::full_capabilities(),
             initializer: None,
             disk_based_diagnostics_progress_token: None,
+            initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
         }
     }
@@ -1730,6 +1758,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
         self.disk_based_diagnostics_progress_token.clone()
     }
+
+    async fn initialization_options(&self) -> Option<Value> {
+        self.initialization_options.clone()
+    }
 }
 
 fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

crates/project/src/project.rs 🔗

@@ -78,8 +78,8 @@ use std::{
 use terminals::Terminals;
 use text::Anchor;
 use util::{
-    debug_panic, defer, http::HttpClient, merge_json_value_into,
-    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt,
+    TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -801,7 +801,7 @@ impl Project {
                         .lsp
                         .get(&adapter.name.0)
                         .and_then(|s| s.initialization_options.as_ref());
-                    if adapter.initialization_options.as_ref() != new_lsp_settings {
+                    if adapter.update_initialization_overrides(new_lsp_settings) {
                         language_servers_to_restart.push((worktree, Arc::clone(language)));
                     }
                 }
@@ -2546,20 +2546,13 @@ impl Project {
         let project_settings = settings::get::<ProjectSettings>(cx);
         let lsp = project_settings.lsp.get(&adapter.name.0);
         let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
-
-        let mut initialization_options = adapter.initialization_options.clone();
-        match (&mut initialization_options, override_options) {
-            (Some(initialization_options), Some(override_options)) => {
-                merge_json_value_into(override_options, initialization_options);
-            }
-            (None, override_options) => initialization_options = override_options,
-            _ => {}
-        }
+        adapter.update_initialization_overrides(override_options.as_ref());
 
         let server_id = pending_server.server_id;
         let container_dir = pending_server.container_dir.clone();
         let state = LanguageServerState::Starting({
             let adapter = adapter.clone();
+            let initialization_options = adapter.initialization_options();
             let server_name = adapter.name.0.clone();
             let languages = self.languages.clone();
             let language = language.clone();

crates/search/src/project_search.rs 🔗

@@ -675,6 +675,9 @@ impl ProjectSearchView {
         if match_ranges.is_empty() {
             self.active_match_index = None;
         } else {
+            self.active_match_index = Some(0);
+            self.select_match(Direction::Next, cx);
+            self.update_match_index(cx);
             let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
             let is_new_search = self.search_id != prev_search_id;
             self.results_editor.update(cx, |editor, cx| {

crates/theme/src/theme.rs 🔗

@@ -1030,6 +1030,7 @@ pub struct AssistantStyle {
     pub system_sender: Interactive<ContainedText>,
     pub model: Interactive<ContainedText>,
     pub remaining_tokens: ContainedText,
+    pub low_remaining_tokens: ContainedText,
     pub no_remaining_tokens: ContainedText,
     pub error_icon: Icon,
     pub api_key_editor: FieldEditor,

crates/vcs_menu/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "vcs_menu"
+version = "0.1.0"
+edition = "2021"
+publish = false
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+fuzzy = {path = "../fuzzy"}
+gpui = {path = "../gpui"}
+picker = {path = "../picker"}
+util = {path = "../util"}
+theme = {path = "../theme"}
+workspace = {path = "../workspace"}
+
+anyhow.workspace = true

crates/collab_ui/src/branch_list.rs → crates/vcs_menu/src/lib.rs 🔗

@@ -1,15 +1,17 @@
-use anyhow::{anyhow, bail};
+use anyhow::{anyhow, bail, Result};
 use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use gpui::{actions, elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::{ops::Not, sync::Arc};
 use util::ResultExt;
 use workspace::{Toast, Workspace};
 
+actions!(branches, [OpenRecent]);
+
 pub fn init(cx: &mut AppContext) {
     Picker::<BranchListDelegate>::init(cx);
+    cx.add_async_action(toggle);
 }
-
 pub type BranchList = Picker<BranchListDelegate>;
 
 pub fn build_branch_list(
@@ -28,6 +30,34 @@ pub fn build_branch_list(
     .with_theme(|theme| theme.picker.clone())
 }
 
+fn toggle(
+    _: &mut Workspace,
+    _: &OpenRecent,
+    cx: &mut ViewContext<Workspace>,
+) -> Option<Task<Result<()>>> {
+    Some(cx.spawn(|workspace, mut cx| async move {
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.toggle_modal(cx, |_, cx| {
+                let workspace = cx.handle();
+                cx.add_view(|cx| {
+                    Picker::new(
+                        BranchListDelegate {
+                            matches: vec![],
+                            workspace,
+                            selected_index: 0,
+                            last_query: String::default(),
+                        },
+                        cx,
+                    )
+                    .with_theme(|theme| theme.picker.clone())
+                    .with_max_size(800., 1200.)
+                })
+            });
+        })?;
+        Ok(())
+    }))
+}
+
 pub struct BranchListDelegate {
     matches: Vec<StringMatch>,
     workspace: ViewHandle<Workspace>,

crates/vim/src/motion.rs 🔗

@@ -31,6 +31,8 @@ pub enum Motion {
     CurrentLine,
     StartOfLine,
     EndOfLine,
+    StartOfParagraph,
+    EndOfParagraph,
     StartOfDocument,
     EndOfDocument,
     Matching,
@@ -72,6 +74,8 @@ actions!(
         StartOfLine,
         EndOfLine,
         CurrentLine,
+        StartOfParagraph,
+        EndOfParagraph,
         StartOfDocument,
         EndOfDocument,
         Matching,
@@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
+        motion(Motion::StartOfParagraph, cx)
+    });
+    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
+        motion(Motion::EndOfParagraph, cx)
+    });
     cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
         motion(Motion::StartOfDocument, cx)
     });
@@ -142,7 +152,8 @@ impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
         match self {
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
+            | StartOfParagraph | EndOfParagraph => true,
             EndOfLine
             | NextWordEnd { .. }
             | Matching
@@ -172,6 +183,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -197,6 +210,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -235,6 +250,14 @@ impl Motion {
             FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
             StartOfLine => (start_of_line(map, point), SelectionGoal::None),
             EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            StartOfParagraph => (
+                movement::start_of_paragraph(map, point, times),
+                SelectionGoal::None,
+            ),
+            EndOfParagraph => (
+                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
+                SelectionGoal::None,
+            ),
             CurrentLine => (end_of_line(map, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
@@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
     if line_end == point {
         line_end = map.max_point().to_point(map);
     }
-    line_end.column = line_end.column.saturating_sub(1);
 
     let line_range = map.prev_line_boundary(point).0..line_end;
-    let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
+    let visible_line_range =
+        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
+    let ranges = map
+        .buffer_snapshot
+        .bracket_ranges(visible_line_range.clone());
     if let Some(ranges) = ranges {
         let line_range = line_range.start.to_offset(&map.buffer_snapshot)
             ..line_range.end.to_offset(&map.buffer_snapshot);
@@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     let new_row = (point.row() + times as u32).min(map.max_buffer_row());
     map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
 }
+
+#[cfg(test)]
+
+mod test {
+
+    use crate::test::NeovimBackedTestContext;
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        let initial_state = indoc! {r"ˇabc
+            def
+
+            paragraph
+            the second
+
+
+
+            third and
+            final"};
+
+        // goes down once
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+            def
+            ˇ
+            paragraph
+            the second
+
+
+
+            third and
+            final"})
+            .await;
+
+        // goes up once
+        cx.simulate_shared_keystrokes(["{"]).await;
+        cx.assert_shared_state(initial_state).await;
+
+        // goes down twice
+        cx.simulate_shared_keystrokes(["2", "}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+            def
+
+            paragraph
+            the second
+            ˇ
+
+
+            third and
+            final"})
+            .await;
+
+        // goes down over multiple blanks
+        cx.simulate_shared_keystrokes(["}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+                def
+
+                paragraph
+                the second
+
+
+
+                third and
+                finaˇl"})
+            .await;
+
+        // goes up twice
+        cx.simulate_shared_keystrokes(["2", "{"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+                def
+                ˇ
+                paragraph
+                the second
+
+
+
+                third and
+                final"})
+            .await
+    }
+
+    #[gpui::test]
+    async fn test_matching(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {r"func ˇ(a string) {
+                do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
+                do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+
+        // test it works on the last character of the line
+        cx.set_shared_state(indoc! {r"func (a string) ˇ{
+            do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state(indoc! {r"func (a string) {
+            do(something(with<Types>.and_arrays[0, 2]))
+            ˇ}"})
+            .await;
+
+        // test it works on immediate nesting
+        cx.set_shared_state("ˇ{()}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("{()ˇ}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("ˇ{()}").await;
+
+        // test it works on immediate nesting inside braces
+        cx.set_shared_state("{\n    ˇ{()}\n}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
+
+        // test it jumps to the next paren on a line
+        cx.set_shared_state("func ˇboop() {\n}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("func boop(ˇ) {\n}").await;
+    }
+}

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

@@ -1,29 +1,51 @@
+use editor::scroll::autoscroll::Autoscroll;
 use gpui::ViewContext;
-use language::Point;
+use language::{Bias, Point};
 use workspace::Workspace;
 
-use crate::{motion::Motion, normal::ChangeCase, Vim};
+use crate::{normal::ChangeCase, state::Mode, Vim};
 
 pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        let count = vim.pop_number_operator(cx);
+        let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
         vim.update_active_editor(cx, |editor, cx| {
-            editor.set_clip_at_line_ends(false, cx);
-            editor.transact(cx, |editor, cx| {
-                editor.change_selections(None, cx, |s| {
-                    s.move_with(|map, selection| {
-                        if selection.start == selection.end {
-                            Motion::Right.expand_selection(map, selection, count, true);
+            let mut ranges = Vec::new();
+            let mut cursor_positions = Vec::new();
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            for selection in editor.selections.all::<Point>(cx) {
+                match vim.state.mode {
+                    Mode::Visual { line: true } => {
+                        let start = Point::new(selection.start.row, 0);
+                        let end =
+                            Point::new(selection.end.row, snapshot.line_len(selection.end.row));
+                        ranges.push(start..end);
+                        cursor_positions.push(start..start);
+                    }
+                    Mode::Visual { line: false } => {
+                        ranges.push(selection.start..selection.end);
+                        cursor_positions.push(selection.start..selection.start);
+                    }
+                    Mode::Insert | Mode::Normal => {
+                        let start = selection.start;
+                        let mut end = start;
+                        for _ in 0..count {
+                            end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
                         }
-                    })
-                });
-                let selections = editor.selections.all::<Point>(cx);
-                for selection in selections.into_iter().rev() {
+                        ranges.push(start..end);
+
+                        if end.column == snapshot.line_len(end.row) {
+                            end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
+                        }
+                        cursor_positions.push(end..end)
+                    }
+                }
+            }
+            editor.transact(cx, |editor, cx| {
+                for range in ranges.into_iter().rev() {
                     let snapshot = editor.buffer().read(cx).snapshot(cx);
                     editor.buffer().update(cx, |buffer, cx| {
-                        let range = selection.start..selection.end;
                         let text = snapshot
-                            .text_for_range(selection.start..selection.end)
+                            .text_for_range(range.start..range.end)
                             .flat_map(|s| s.chars())
                             .flat_map(|c| {
                                 if c.is_lowercase() {
@@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
                         buffer.edit([(range, text)], None, cx)
                     })
                 }
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.select_ranges(cursor_positions)
+                })
             });
-            editor.set_clip_at_line_ends(true, cx);
         });
+        vim.switch_mode(Mode::Normal, true, cx)
     })
 }
-
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
-    use indoc::indoc;
+    use crate::{state::Mode, test::NeovimBackedTestContext};
 
     #[gpui::test]
     async fn test_change_case(cx: &mut gpui::TestAppContext) {
-        let mut cx = VimTestContext::new(cx, true).await;
-        cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
-        cx.simulate_keystrokes(["~"]);
-        cx.assert_editor_state("AˇbC\n");
-        cx.simulate_keystrokes(["2", "~"]);
-        cx.assert_editor_state("ABcˇ\n");
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("ˇabC\n").await;
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.assert_shared_state("AˇbC\n").await;
+        cx.simulate_shared_keystrokes(["2", "~"]).await;
+        cx.assert_shared_state("ABˇc\n").await;
+
+        // works in visual mode
+        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.assert_shared_state("a😀CˇDé1*F\n").await;
+
+        // works with multibyte characters
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.set_shared_state("aˇC😀é1*F\n").await;
+        cx.simulate_shared_keystrokes(["4", "~"]).await;
+        cx.assert_shared_state("ac😀É1ˇ*F\n").await;
+
+        // works with line selections
+        cx.set_shared_state("abˇC\n").await;
+        cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
+        cx.assert_shared_state("ˇABc\n").await;
 
-        cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
-        cx.simulate_keystrokes(["~"]);
-        cx.assert_editor_state("a😀CDé1*Fˇ\n");
+        // works with multiple cursors (zed only)
+        cx.set_state("aˇßcdˇe\n", Mode::Normal);
+        cx.simulate_keystroke("~");
+        cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
     }
 }

crates/vim/src/test.rs 🔗

@@ -4,6 +4,7 @@ mod neovim_connection;
 mod vim_binding_test_context;
 mod vim_test_context;
 
+use command_palette::CommandPalette;
 pub use neovim_backed_binding_test_context::*;
 pub use neovim_backed_test_context::*;
 pub use vim_binding_test_context::*;
@@ -139,3 +140,16 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
     cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
     cx.assert_editor_state("aa\n    b«b\n    cˇ»c");
 }
+
+#[gpui::test]
+async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state("aˇbc\n", Mode::Normal);
+    cx.simulate_keystrokes(["i", "cmd-shift-p"]);
+
+    assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
+    cx.simulate_keystroke("escape");
+    assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
+    cx.assert_state("aˇbc\n", Mode::Insert);
+}

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

@@ -1,9 +1,10 @@
-use std::ops::{Deref, DerefMut};
+use indoc::indoc;
+use std::ops::{Deref, DerefMut, Range};
 
 use collections::{HashMap, HashSet};
 use gpui::ContextHandle;
 use language::OffsetRangeExt;
-use util::test::marked_text_offsets;
+use util::test::{generate_marked_text, marked_text_offsets};
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
@@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> {
         context_handle
     }
 
+    pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        let neovim = self.neovim_state().await;
+        if neovim != marked_text {
+            panic!(
+                indoc! {"Test is incorrect (currently expected != neovim state)
+
+                # currently expected:
+                {}
+                # neovim state:
+                {}
+                # zed state:
+                {}"},
+                marked_text,
+                neovim,
+                self.editor_state(),
+            )
+        }
+        self.assert_editor_state(marked_text)
+    }
+
+    pub async fn neovim_state(&mut self) -> String {
+        generate_marked_text(
+            self.neovim.text().await.as_str(),
+            &vec![self.neovim_selection().await],
+            true,
+        )
+    }
+
+    async fn neovim_selection(&mut self) -> Range<usize> {
+        let mut neovim_selection = self.neovim.selection().await;
+        // Zed selections adjust themselves to make the end point visually make sense
+        if neovim_selection.start > neovim_selection.end {
+            neovim_selection.start.column += 1;
+        }
+        neovim_selection.to_offset(&self.buffer_snapshot())
+    }
+
     pub async fn assert_state_matches(&mut self) {
         assert_eq!(
             self.neovim.text().await,
@@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> {
             self.assertion_context()
         );
 
-        let mut neovim_selection = self.neovim.selection().await;
-        // Zed selections adjust themselves to make the end point visually make sense
-        if neovim_selection.start > neovim_selection.end {
-            neovim_selection.start.column += 1;
-        }
-        let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
-        self.assert_editor_selections(vec![neovim_selection]);
+        let selections = vec![self.neovim_selection().await];
+        self.assert_editor_selections(selections);
 
         if let Some(neovim_mode) = self.neovim.mode().await {
             assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);

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

@@ -167,15 +167,25 @@ impl NeovimConnection {
             .await
             .expect("Could not get neovim window");
 
-        if !selection.is_empty() {
-            panic!("Setting neovim state with non empty selection not yet supported");
-        }
         let cursor = selection.start;
         nvim_window
             .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
             .await
             .expect("Could not set nvim cursor position");
 
+        if !selection.is_empty() {
+            self.nvim
+                .input("v")
+                .await
+                .expect("could not enter visual mode");
+
+            let cursor = selection.end;
+            nvim_window
+                .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
+                .await
+                .expect("Could not set nvim cursor position");
+        }
+
         if let Some(NeovimData::Get { mode, state }) = self.data.back() {
             if *mode == Some(Mode::Normal) && *state == marked_text {
                 return;

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

@@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> {
         cx.update(|cx| {
             search::init(cx);
             crate::init(cx);
+            command_palette::init(cx);
         });
 
         cx.update(|cx| {
             cx.update_global(|store: &mut SettingsStore, cx| {
                 store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
             });
+            settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
             settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
         });
 

crates/vim/src/vim.rs 🔗

@@ -12,7 +12,7 @@ mod visual;
 
 use anyhow::Result;
 use collections::CommandPaletteFilter;
-use editor::{Bias, Cancel, Editor, EditorMode, Event};
+use editor::{Bias, Editor, EditorMode, Event};
 use gpui::{
     actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
     WindowContext,
@@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) {
         Vim::update(cx, |vim, cx| vim.push_number(n, cx));
     });
 
-    // Editor Actions
-    cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
-        // If we are in aren't in normal mode or have an active operator, swap to normal mode
-        // Otherwise forward cancel on to the editor
-        let vim = Vim::read(cx);
-        if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
-            WindowContext::defer(cx, |cx| {
-                Vim::update(cx, |state, cx| {
-                    state.switch_mode(Mode::Normal, false, cx);
-                });
-            });
-        } else {
-            cx.propagate_action();
-        }
-    });
-
     cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
         Vim::active_editor_input_ignored(" ".into(), cx)
     });
@@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
     cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
         if let Some(handled_by) = handled_by {
             // Keystroke is handled by the vim system, so continue forward
-            // Also short circuit if it is the special cancel action
-            if handled_by.namespace() == "vim"
-                || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
-            {
+            if handled_by.namespace() == "vim" {
                 return true;
             }
         }

crates/vim/test_data/test_change_case.json 🔗

@@ -0,0 +1,18 @@
+{"Put":{"state":"ˇabC\n"}}
+{"Key":"~"}
+{"Get":{"state":"AˇbC\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"~"}
+{"Get":{"state":"ABˇc\n","mode":"Normal"}}
+{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
+{"Key":"~"}
+{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
+{"Key":"~"}
+{"Put":{"state":"aˇC😀é1*F\n"}}
+{"Key":"4"}
+{"Key":"~"}
+{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
+{"Put":{"state":"abˇC\n"}}
+{"Key":"shift-v"}
+{"Key":"~"}
+{"Get":{"state":"ˇABc\n","mode":"Normal"}}

crates/vim/test_data/test_matching.json 🔗

@@ -0,0 +1,17 @@
+{"Put":{"state":"func ˇ(a string) {\n    do(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a stringˇ) {\n    do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
+{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
+{"Put":{"state":"ˇ{()}"}}
+{"Key":"%"}
+{"Get":{"state":"{()ˇ}","mode":"Normal"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{()}","mode":"Normal"}}
+{"Put":{"state":"{\n    ˇ{()}\n}"}}
+{"Key":"%"}
+{"Get":{"state":"{\n    {()ˇ}\n}","mode":"Normal"}}
+{"Put":{"state":"func ˇboop() {\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}

crates/vim/test_data/test_start_end_of_paragraph.json 🔗

@@ -0,0 +1,13 @@
+{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"{"}
+{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"{"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}

crates/zed/src/main.rs 🔗

@@ -57,8 +57,9 @@ use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
 use zed::{
-    assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
-    languages, menus,
+    assets::Assets,
+    build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+    only_instance::{ensure_only_instance, IsOnlyInstance},
 };
 
 fn main() {
@@ -66,6 +67,10 @@ fn main() {
     init_paths();
     init_logger();
 
+    if ensure_only_instance() != IsOnlyInstance::Yes {
+        return;
+    }
+
     log::info!("========== starting zed ==========");
     let mut app = gpui::App::new(Assets).unwrap();
 

crates/zed/src/only_instance.rs 🔗

@@ -0,0 +1,103 @@
+use std::{
+    io::{Read, Write},
+    net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
+    thread,
+    time::Duration,
+};
+
+use util::channel::ReleaseChannel;
+
+const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
+const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
+const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
+const SEND_TIMEOUT: Duration = Duration::from_millis(20);
+
+fn address() -> SocketAddr {
+    let port = match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => 43737,
+        ReleaseChannel::Preview => 43738,
+        ReleaseChannel::Stable => 43739,
+    };
+
+    SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
+}
+
+fn instance_handshake() -> &'static str {
+    match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
+        ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
+        ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum IsOnlyInstance {
+    Yes,
+    No,
+}
+
+pub fn ensure_only_instance() -> IsOnlyInstance {
+    if *db::ZED_STATELESS {
+        return IsOnlyInstance::Yes;
+    }
+
+    if check_got_handshake() {
+        return IsOnlyInstance::No;
+    }
+
+    let listener = match TcpListener::bind(address()) {
+        Ok(listener) => listener,
+
+        Err(err) => {
+            log::warn!("Error binding to single instance port: {err}");
+            if check_got_handshake() {
+                return IsOnlyInstance::No;
+            }
+
+            // Avoid failing to start when some other application by chance already has
+            // a claim on the port. This is sub-par as any other instance that gets launched
+            // will be unable to communicate with this instance and will duplicate
+            log::warn!("Backup handshake request failed, continuing without handshake");
+            return IsOnlyInstance::Yes;
+        }
+    };
+
+    thread::spawn(move || {
+        for stream in listener.incoming() {
+            let mut stream = match stream {
+                Ok(stream) => stream,
+                Err(_) => return,
+            };
+
+            _ = stream.set_nodelay(true);
+            _ = stream.set_read_timeout(Some(SEND_TIMEOUT));
+            _ = stream.write_all(instance_handshake().as_bytes());
+        }
+    });
+
+    IsOnlyInstance::Yes
+}
+
+fn check_got_handshake() -> bool {
+    match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
+        Ok(mut stream) => {
+            let mut buf = vec![0u8; instance_handshake().len()];
+
+            stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
+            if let Err(err) = stream.read_exact(&mut buf) {
+                log::warn!("Connected to single instance port but failed to read: {err}");
+                return false;
+            }
+
+            if buf == instance_handshake().as_bytes() {
+                log::info!("Got instance handshake");
+                return true;
+            }
+
+            log::warn!("Got wrong instance handshake value");
+            false
+        }
+
+        Err(_) => false,
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod languages;
 pub mod menus;
+pub mod only_instance;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 

styles/src/component/tab_bar_button.ts 🔗

@@ -0,0 +1,55 @@
+import { Theme, StyleSets } from "../common"
+import { interactive } from "../element"
+import { InteractiveState } from "../element/interactive"
+import { background, foreground } from "../style_tree/components"
+
+interface TabBarButtonOptions {
+    icon: string
+    color?: StyleSets
+}
+
+type TabBarButtonProps = TabBarButtonOptions & {
+    state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
+}
+
+export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
+    const button_spacing = 8
+
+    return (
+        interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.middle, color),
+                    asset: icon,
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    corner_radius: 4,
+                    padding: {
+                        top: 4, bottom: 4, left: 4, right: 4
+                    },
+                    margin: {
+                        left: button_spacing / 2,
+                        right: button_spacing / 2,
+                    },
+                },
+            },
+            state: {
+                hovered: {
+                    container: {
+                        background: background(theme.middle, color, "hovered"),
+
+                    }
+                },
+                clicked: {
+                    container: {
+                        background: background(theme.middle, color, "pressed"),
+                    }
+                },
+            },
+        })
+    )
+}

styles/src/style_tree/assistant.ts 🔗

@@ -1,233 +1,133 @@
-import { text, border, background, foreground } from "./components"
-import { interactive } from "../element"
-import { useTheme } from "../theme"
+import { text, border, background, foreground, TextStyle } from "./components"
+import { Interactive, interactive } from "../element"
+import { tab_bar_button } from "../component/tab_bar_button"
+import { StyleSets, useTheme } from "../theme"
+
+type RoleCycleButton = TextStyle & {
+    background?: string
+}
+// TODO: Replace these with zed types
+type RemainingTokens = TextStyle & {
+    background: string,
+    margin: { top: number, right: number },
+    padding: {
+        right: number,
+        left: number,
+        top: number,
+        bottom: number,
+    },
+    corner_radius: number,
+}
 
 export default function assistant(): any {
     const theme = useTheme()
 
+    const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
+        return (
+            interactive({
+                base: {
+                    ...text(theme.highest, "sans", color, { size: "sm" }),
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "pressed"),
+                    }
+                },
+            })
+        )
+    }
+
+    const tokens_remaining = (color: StyleSets): RemainingTokens => {
+        return (
+            {
+                ...text(theme.highest, "mono", color, { size: "xs" }),
+                background: background(theme.highest, "on", "default"),
+                margin: { top: 12, right: 20 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
+                corner_radius: 6,
+            }
+        )
+    }
+
     return {
         container: {
             background: background(theme.highest),
             padding: { left: 12 },
         },
         message_header: {
-            margin: { bottom: 6, top: 6 },
+            margin: { bottom: 4, top: 4 },
             background: background(theme.highest),
         },
-        hamburger_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/hamburger_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 12, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        hamburger_button: tab_bar_button(theme, {
+            icon: "icons/hamburger_15.svg",
         }),
-        split_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/split_message_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+
+        split_button: tab_bar_button(theme, {
+            icon: "icons/split_message_15.svg",
         }),
-        quote_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/quote_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        quote_button: tab_bar_button(theme, {
+            icon: "icons/radix/quote.svg",
         }),
-        assist_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/assist_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        assist_button: tab_bar_button(theme, {
+            icon: "icons/radix/magic-wand.svg",
         }),
-        zoom_in_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/maximize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_in_button: tab_bar_button(theme, {
+            icon: "icons/radix/maximize.svg",
         }),
-        zoom_out_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/minimize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_out_button: tab_bar_button(theme, {
+            icon: "icons/radix/minimize.svg",
         }),
-        plus_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/plus_12.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        plus_button: tab_bar_button(theme, {
+            icon: "icons/radix/plus.svg",
         }),
         title: {
-            ...text(theme.highest, "sans", "default", { size: "sm" }),
+            ...text(theme.highest, "sans", "default", { size: "xs" }),
         },
         saved_conversation: {
             container: interactive({
                 base: {
-                    background: background(theme.highest, "on"),
+                    background: background(theme.middle),
                     padding: { top: 4, bottom: 4 },
+                    border: border(theme.middle, "default", { top: true, overlay: true }),
                 },
                 state: {
                     hovered: {
-                        background: background(theme.highest, "on", "hovered"),
+                        background: background(theme.middle, "hovered"),
                     },
+                    clicked: {
+                        background: background(theme.middle, "pressed"),
+                    }
                 },
             }),
             saved_at: {
                 margin: { left: 8 },
-                ...text(theme.highest, "sans", "default", { size: "xs" }),
+                ...text(theme.highest, "sans", "variant", { size: "xs" }),
             },
             title: {
-                margin: { left: 16 },
-                ...text(theme.highest, "sans", "default", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        user_sender: {
-            default: {
+                margin: { left: 12 },
                 ...text(theme.highest, "sans", "default", {
                     size: "sm",
                     weight: "bold",
                 }),
             },
         },
-        assistant_sender: {
-            default: {
-                ...text(theme.highest, "sans", "accent", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        system_sender: {
-            default: {
-                ...text(theme.highest, "sans", "variant", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
+        user_sender: interactive_role("base"),
+        assistant_sender: interactive_role("accent"),
+        system_sender: interactive_role("warning"),
         sent_at: {
             margin: { top: 2, left: 8 },
-            ...text(theme.highest, "sans", "default", { size: "2xs" }),
+            ...text(theme.highest, "sans", "variant", { size: "2xs" }),
         },
         model: interactive({
             base: {
-                background: background(theme.highest, "on"),
-                margin: { left: 12, right: 12, top: 12 },
-                padding: 4,
+                background: background(theme.highest),
+                margin: { left: 12, right: 4, top: 12 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
                 corner_radius: 4,
                 ...text(theme.highest, "sans", "default", { size: "xs" }),
             },
@@ -238,20 +138,9 @@ export default function assistant(): any {
                 },
             },
         }),
-        remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "positive", { size: "xs" }),
-        },
-        no_remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "negative", { size: "xs" }),
-        },
+        remaining_tokens: tokens_remaining("positive"),
+        low_remaining_tokens: tokens_remaining("warning"),
+        no_remaining_tokens: tokens_remaining("negative"),
         error_icon: {
             margin: { left: 8 },
             color: foreground(theme.highest, "negative"),
@@ -259,7 +148,7 @@ export default function assistant(): any {
         },
         api_key_editor: {
             background: background(theme.highest, "on"),
-            corner_radius: 6,
+            corner_radius: 4,
             text: text(theme.highest, "mono", "on"),
             placeholder_text: text(theme.highest, "mono", "on", "disabled", {
                 size: "xs",

styles/src/theme/create_theme.ts 🔗

@@ -12,8 +12,17 @@ export interface Theme {
     name: string
     is_light: boolean
 
+    /**
+    * App background, other elements that should sit directly on top of the background.
+    */
     lowest: Layer
+    /**
+    * Panels, tabs, other UI surfaces that sit on top of the background.
+    */
     middle: Layer
+    /**
+    * Editors like code buffers, conversation editors, etc.
+    */
     highest: Layer
 
     ramps: RampSet