diff --git a/Cargo.lock b/Cargo.lock
index a0be9756bf..cb71ea51dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -126,18 +126,17 @@ dependencies = [
[[package]]
name = "alacritty_config"
version = "0.1.2-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5"
dependencies = [
"log",
"serde",
"toml 0.7.6",
- "winit",
]
[[package]]
name = "alacritty_config_derive"
version = "0.2.2-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5"
dependencies = [
"proc-macro2",
"quote",
@@ -147,7 +146,7 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.20.0-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5"
dependencies = [
"alacritty_config",
"alacritty_config_derive",
@@ -213,30 +212,6 @@ version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
-[[package]]
-name = "android-activity"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0"
-dependencies = [
- "android-properties",
- "bitflags 1.3.2",
- "cc",
- "jni-sys",
- "libc",
- "log",
- "ndk",
- "ndk-context",
- "ndk-sys",
- "num_enum 0.6.1",
-]
-
-[[package]]
-name = "android-properties"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
-
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -926,25 +901,6 @@ dependencies = [
"generic-array",
]
-[[package]]
-name = "block-sys"
-version = "0.1.0-beta.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146"
-dependencies = [
- "objc-sys",
-]
-
-[[package]]
-name = "block2"
-version = "0.2.0-alpha.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42"
-dependencies = [
- "block-sys",
- "objc2-encode",
-]
-
[[package]]
name = "blocking"
version = "1.3.1"
@@ -1126,20 +1082,6 @@ dependencies = [
"util",
]
-[[package]]
-name = "calloop"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8"
-dependencies = [
- "bitflags 1.3.2",
- "log",
- "nix 0.25.1",
- "slotmap",
- "thiserror",
- "vec_map",
-]
-
[[package]]
name = "cap-fs-ext"
version = "0.24.4"
@@ -1248,12 +1190,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-[[package]]
-name = "cfg_aliases"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
-
[[package]]
name = "chrono"
version = "0.4.26"
@@ -1479,7 +1415,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.16.0"
+version = "0.17.0"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1552,6 +1488,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
+ "db",
"editor",
"feedback",
"futures 0.3.28",
@@ -1563,9 +1500,11 @@ dependencies = [
"postage",
"project",
"recent_projects",
+ "schemars",
"serde",
"serde_derive",
"settings",
+ "staff_mode",
"theme",
"theme_selector",
"util",
@@ -2070,15 +2009,6 @@ dependencies = [
"winapi 0.3.9",
]
-[[package]]
-name = "cursor-icon"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf"
-dependencies = [
- "serde",
-]
-
[[package]]
name = "dashmap"
version = "5.5.0"
@@ -2285,12 +2215,6 @@ dependencies = [
"winapi 0.3.9",
]
-[[package]]
-name = "dispatch"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
-
[[package]]
name = "dlib"
version = "0.5.2"
@@ -4530,7 +4454,7 @@ dependencies = [
"bitflags 1.3.2",
"jni-sys",
"ndk-sys",
- "num_enum 0.5.11",
+ "num_enum",
"raw-window-handle",
"thiserror",
]
@@ -4572,19 +4496,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "nix"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
-dependencies = [
- "autocfg",
- "bitflags 1.3.2",
- "cfg-if 1.0.0",
- "libc",
- "memoffset 0.6.5",
-]
-
[[package]]
name = "nix"
version = "0.26.2"
@@ -4750,16 +4661,7 @@ version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
- "num_enum_derive 0.5.11",
-]
-
-[[package]]
-name = "num_enum"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1"
-dependencies = [
- "num_enum_derive 0.6.1",
+ "num_enum_derive",
]
[[package]]
@@ -4774,18 +4676,6 @@ dependencies = [
"syn 1.0.109",
]
-[[package]]
-name = "num_enum_derive"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
-dependencies = [
- "proc-macro-crate 1.3.1",
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
[[package]]
name = "nvim-rs"
version = "0.5.0"
@@ -4811,32 +4701,6 @@ dependencies = [
"objc_exception",
]
-[[package]]
-name = "objc-sys"
-version = "0.2.0-beta.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7"
-
-[[package]]
-name = "objc2"
-version = "0.3.0-beta.3.patch-leaks.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468"
-dependencies = [
- "block2",
- "objc-sys",
- "objc2-encode",
-]
-
-[[package]]
-name = "objc2-encode"
-version = "2.0.0-pre.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512"
-dependencies = [
- "objc-sys",
-]
-
[[package]]
name = "objc_exception"
version = "0.1.2"
@@ -4955,15 +4819,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "orbclient"
-version = "0.3.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1"
-dependencies = [
- "redox_syscall 0.3.5",
-]
-
[[package]]
name = "ordered-float"
version = "2.10.0"
@@ -5711,6 +5566,17 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quick_action_bar"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "gpui",
+ "search",
+ "theme",
+ "workspace",
+]
+
[[package]]
name = "quote"
version = "1.0.32"
@@ -7092,15 +6958,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
-[[package]]
-name = "slotmap"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
-dependencies = [
- "version_check",
-]
-
[[package]]
name = "sluice"
version = "0.5.5"
@@ -7145,15 +7002,6 @@ dependencies = [
"pin-project-lite 0.1.12",
]
-[[package]]
-name = "smol_str"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c"
-dependencies = [
- "serde",
-]
-
[[package]]
name = "snippet"
version = "0.1.0"
@@ -8840,12 +8688,6 @@ dependencies = [
"workspace",
]
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
[[package]]
name = "version_check"
version = "0.9.4"
@@ -9310,17 +9152,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "web-time"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19353897b48e2c4d849a2d73cb0aeb16dc2be4e00c565abfc11eb65a806e47de"
-dependencies = [
- "js-sys",
- "once_cell",
- "wasm-bindgen",
-]
-
[[package]]
name = "webpki"
version = "0.21.4"
@@ -9636,42 +9467,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
-[[package]]
-name = "winit"
-version = "0.29.0-beta.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f1afaf8490cc3f1309520ebb53a4cd3fc3642c7df8064a4b074bb9867998d44"
-dependencies = [
- "android-activity",
- "atomic-waker",
- "bitflags 2.3.3",
- "calloop",
- "cfg_aliases",
- "core-foundation",
- "core-graphics",
- "cursor-icon",
- "dispatch",
- "js-sys",
- "libc",
- "log",
- "ndk",
- "ndk-sys",
- "objc2",
- "once_cell",
- "orbclient",
- "raw-window-handle",
- "redox_syscall 0.3.5",
- "serde",
- "smol_str",
- "unicode-segmentation",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "web-time",
- "windows-sys",
- "xkbcommon-dl",
-]
-
[[package]]
name = "winnow"
version = "0.5.2"
@@ -9789,25 +9584,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "xkbcommon-dl"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699"
-dependencies = [
- "bitflags 2.3.3",
- "dlib",
- "log",
- "once_cell",
- "xkeysym",
-]
-
-[[package]]
-name = "xkeysym"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
-
[[package]]
name = "xmlparser"
version = "0.13.5"
@@ -9860,7 +9636,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.100.0"
+version = "0.101.0"
dependencies = [
"activity_indicator",
"ai",
@@ -9919,6 +9695,7 @@ dependencies = [
"project",
"project_panel",
"project_symbols",
+ "quick_action_bar",
"rand 0.8.5",
"recent_projects",
"regex",
diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg
new file mode 100644
index 0000000000..5b3faaa9cc
--- /dev/null
+++ b/assets/icons/ai.svg
@@ -0,0 +1,23 @@
+
diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg
new file mode 100644
index 0000000000..186c9c7457
--- /dev/null
+++ b/assets/icons/arrow_left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg
new file mode 100644
index 0000000000..7bae7f4801
--- /dev/null
+++ b/assets/icons/arrow_right.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/channel_hash.svg b/assets/icons/channel_hash.svg
new file mode 100644
index 0000000000..edd0462678
--- /dev/null
+++ b/assets/icons/channel_hash.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 0000000000..77b180892c
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg
new file mode 100644
index 0000000000..85ba2e1f37
--- /dev/null
+++ b/assets/icons/check_circle.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg
new file mode 100644
index 0000000000..b971555cfa
--- /dev/null
+++ b/assets/icons/chevron_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg
new file mode 100644
index 0000000000..8e61beed5d
--- /dev/null
+++ b/assets/icons/chevron_left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg
new file mode 100644
index 0000000000..fcd9d83fc2
--- /dev/null
+++ b/assets/icons/chevron_right.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg
new file mode 100644
index 0000000000..171cdd61c0
--- /dev/null
+++ b/assets/icons/chevron_up.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/conversations.svg b/assets/icons/conversations.svg
new file mode 100644
index 0000000000..fe8ad03dda
--- /dev/null
+++ b/assets/icons/conversations.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg
new file mode 100644
index 0000000000..06dbf178ae
--- /dev/null
+++ b/assets/icons/copilot.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 0000000000..4aa44979c3
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg
new file mode 100644
index 0000000000..1858c65520
--- /dev/null
+++ b/assets/icons/ellipsis.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/error.svg b/assets/icons/error.svg
new file mode 100644
index 0000000000..82b9401d08
--- /dev/null
+++ b/assets/icons/error.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg
new file mode 100644
index 0000000000..7e45535773
--- /dev/null
+++ b/assets/icons/exit.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/feedback.svg b/assets/icons/feedback.svg
new file mode 100644
index 0000000000..2703f70119
--- /dev/null
+++ b/assets/icons/feedback.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg
new file mode 100644
index 0000000000..80ce656f57
--- /dev/null
+++ b/assets/icons/filter.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg
new file mode 100644
index 0000000000..f685245ed3
--- /dev/null
+++ b/assets/icons/hash.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/html.svg b/assets/icons/html.svg
new file mode 100644
index 0000000000..1e676fe313
--- /dev/null
+++ b/assets/icons/html.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/inlay_hint.svg b/assets/icons/inlay_hint.svg
new file mode 100644
index 0000000000..c8e6bb2d36
--- /dev/null
+++ b/assets/icons/inlay_hint.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/kebab.svg b/assets/icons/kebab.svg
new file mode 100644
index 0000000000..1858c65520
--- /dev/null
+++ b/assets/icons/kebab.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg
new file mode 100644
index 0000000000..652f45a7e8
--- /dev/null
+++ b/assets/icons/lock.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg
new file mode 100644
index 0000000000..0b539adb6c
--- /dev/null
+++ b/assets/icons/magnifying_glass.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/match_case.svg b/assets/icons/match_case.svg
new file mode 100644
index 0000000000..82f4529c1b
--- /dev/null
+++ b/assets/icons/match_case.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/match_word.svg b/assets/icons/match_word.svg
new file mode 100644
index 0000000000..69ba8eb9e6
--- /dev/null
+++ b/assets/icons/match_word.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg
new file mode 100644
index 0000000000..4dc7755714
--- /dev/null
+++ b/assets/icons/maximize.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg
new file mode 100644
index 0000000000..8974fd939d
--- /dev/null
+++ b/assets/icons/microphone.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg
new file mode 100644
index 0000000000..d8941ee1f0
--- /dev/null
+++ b/assets/icons/minimize.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg
new file mode 100644
index 0000000000..a54dd0ad66
--- /dev/null
+++ b/assets/icons/plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/project.svg b/assets/icons/project.svg
new file mode 100644
index 0000000000..525109db4c
--- /dev/null
+++ b/assets/icons/project.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg
new file mode 100644
index 0000000000..af10921891
--- /dev/null
+++ b/assets/icons/replace.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/replace_all.svg b/assets/icons/replace_all.svg
new file mode 100644
index 0000000000..4838e82242
--- /dev/null
+++ b/assets/icons/replace_all.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg
new file mode 100644
index 0000000000..ba751411af
--- /dev/null
+++ b/assets/icons/replace_next.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg
new file mode 100644
index 0000000000..49e097b023
--- /dev/null
+++ b/assets/icons/screen.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/split.svg b/assets/icons/split.svg
new file mode 100644
index 0000000000..4c131466c2
--- /dev/null
+++ b/assets/icons/split.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/success.svg b/assets/icons/success.svg
new file mode 100644
index 0000000000..85450cdc43
--- /dev/null
+++ b/assets/icons/success.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/terminal.svg b/assets/icons/terminal.svg
new file mode 100644
index 0000000000..15dd705b0b
--- /dev/null
+++ b/assets/icons/terminal.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg
new file mode 100644
index 0000000000..6b3d0fd41e
--- /dev/null
+++ b/assets/icons/warning.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/x.svg b/assets/icons/x.svg
new file mode 100644
index 0000000000..31c5aa31a6
--- /dev/null
+++ b/assets/icons/x.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json
index d0fad11503..3ec994335e 100644
--- a/assets/keymaps/default.json
+++ b/assets/keymaps/default.json
@@ -13,6 +13,7 @@
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
+ "ctrl-enter": "menu::ShowContextMenu",
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
@@ -517,7 +518,8 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
- "cmd-shift-c": "collab::ToggleContactsMenu",
+ // TODO: Move this to a dock open action
+ "cmd-shift-c": "collab_panel::ToggleFocus",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -553,6 +555,25 @@
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
+ {
+ "context": "CollabPanel",
+ "bindings": {
+ "ctrl-backspace": "collab_panel::Remove",
+ "space": "menu::Confirm"
+ }
+ },
+ {
+ "context": "ChannelModal",
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "ChannelModal > Picker > Editor",
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
{
"context": "Terminal",
"bindings": {
diff --git a/assets/settings/default.json b/assets/settings/default.json
index c6235e80a1..2ddf4a137f 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -122,7 +122,17 @@
// Amount of indentation for nested items.
"indent_size": 20
},
+ "collaboration_panel": {
+ // Whether to show the collaboration panel button in the status bar.
+ "button": true,
+ // Where to dock channels panel. Can be 'left' or 'right'.
+ "dock": "right",
+ // Default width of the channels panel.
+ "default_width": 240
+ },
"assistant": {
+ // Whether to show the assistant panel button in the status bar.
+ "button": true,
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs
index bced6021ba..e5026182ed 100644
--- a/crates/ai/src/assistant.rs
+++ b/crates/ai/src/assistant.rs
@@ -192,6 +192,7 @@ impl AssistantPanel {
old_dock_position = new_dock_position;
cx.emit(AssistantPanelEvent::DockPositionChanged);
}
+ cx.notify();
})];
this
@@ -725,10 +726,10 @@ impl Panel for AssistantPanel {
}
}
- fn set_size(&mut self, size: f32, cx: &mut ViewContext) {
+ fn set_size(&mut self, size: Option, cx: &mut ViewContext) {
match self.position(cx) {
- DockPosition::Left | DockPosition::Right => self.width = Some(size),
- DockPosition::Bottom => self.height = Some(size),
+ DockPosition::Left | DockPosition::Right => self.width = size,
+ DockPosition::Bottom => self.height = size,
}
cx.notify();
}
@@ -780,8 +781,10 @@ impl Panel for AssistantPanel {
}
}
- fn icon_path(&self) -> &'static str {
- "icons/robot_14.svg"
+ fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+ settings::get::(cx)
+ .button
+ .then(|| "icons/ai.svg")
}
fn icon_tooltip(&self) -> (String, Option>) {
diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs
index eb92e0f6e8..04ba8fb946 100644
--- a/crates/ai/src/assistant_settings.rs
+++ b/crates/ai/src/assistant_settings.rs
@@ -13,6 +13,7 @@ pub enum AssistantDockPosition {
#[derive(Deserialize, Debug)]
pub struct AssistantSettings {
+ pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: f32,
pub default_height: f32,
@@ -20,6 +21,7 @@ pub struct AssistantSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
+ pub button: Option,
pub dock: Option,
pub default_width: Option,
pub default_height: Option,
diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs
index 233b0f62aa..d80fb6738f 100644
--- a/crates/audio/src/audio.rs
+++ b/crates/audio/src/audio.rs
@@ -39,29 +39,43 @@ pub struct Audio {
impl Audio {
pub fn new() -> Self {
- let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
-
Self {
- _output_stream,
- output_handle,
+ _output_stream: None,
+ output_handle: None,
}
}
- pub fn play_sound(sound: Sound, cx: &AppContext) {
+ fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
+ if self.output_handle.is_none() {
+ let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+ self.output_handle = output_handle;
+ self._output_stream = _output_stream;
+ }
+
+ self.output_handle.as_ref()
+ }
+
+ pub fn play_sound(sound: Sound, cx: &mut AppContext) {
if !cx.has_global::() {
return;
}
- let this = cx.global::();
+ cx.update_global::(|this, cx| {
+ let output_handle = this.ensure_output_exists()?;
+ let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
+ output_handle.play_raw(source).log_err()?;
+ Some(())
+ });
+ }
- let Some(output_handle) = this.output_handle.as_ref() else {
+ pub fn end_call(cx: &mut AppContext) {
+ if !cx.has_global::() {
return;
- };
+ }
- let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
- return;
- };
-
- output_handle.play_raw(source).log_err();
+ cx.update_global::(|this, _| {
+ this._output_stream.take();
+ this.output_handle.take();
+ });
}
}
diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs
index 2defd6b40f..3ac29bfc85 100644
--- a/crates/call/src/call.rs
+++ b/crates/call/src/call.rs
@@ -5,8 +5,11 @@ pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
+use audio::Audio;
use call_settings::CallSettings;
-use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
+use client::{
+ proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
@@ -75,6 +78,10 @@ impl ActiveCall {
}
}
+ pub fn channel_id(&self, cx: &AppContext) -> Option {
+ self.room()?.read(cx).channel_id()
+ }
+
async fn handle_incoming_call(
this: ModelHandle,
envelope: TypedEnvelope,
@@ -274,9 +281,36 @@ impl ActiveCall {
Ok(())
}
+ pub fn join_channel(
+ &mut self,
+ channel_id: u64,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if let Some(room) = self.room().cloned() {
+ if room.read(cx).channel_id() == Some(channel_id) {
+ return Task::ready(Ok(()));
+ } else {
+ room.update(cx, |room, cx| room.clear_state(cx));
+ }
+ }
+
+ let join = Room::join_channel(channel_id, 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("join channel", cx)
+ });
+ Ok(())
+ })
+ }
+
pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> {
cx.notify();
self.report_call_event("hang up", cx);
+ Audio::end_call(cx);
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))
} else {
diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs
index 328a94506c..6f01b1d757 100644
--- a/crates/call/src/room.rs
+++ b/crates/call/src/room.rs
@@ -49,6 +49,7 @@ pub enum Event {
pub struct Room {
id: u64,
+ channel_id: Option,
live_kit: Option,
status: RoomStatus,
shared_projects: HashSet>,
@@ -93,8 +94,25 @@ impl Entity for Room {
}
impl Room {
+ pub fn channel_id(&self) -> Option {
+ self.channel_id
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn is_connected(&self) -> bool {
+ if let Some(live_kit) = self.live_kit.as_ref() {
+ matches!(
+ *live_kit.room.status().borrow(),
+ live_kit_client::ConnectionState::Connected { .. }
+ )
+ } else {
+ false
+ }
+ }
+
fn new(
id: u64,
+ channel_id: Option,
live_kit_connection_info: Option,
client: Arc,
user_store: ModelHandle,
@@ -185,6 +203,7 @@ impl Room {
Self {
id,
+ channel_id,
live_kit: live_kit_room,
status: RoomStatus::Online,
shared_projects: Default::default(),
@@ -217,6 +236,7 @@ impl Room {
let room = cx.add_model(|cx| {
Self::new(
room_proto.id,
+ None,
response.live_kit_connection_info,
client,
user_store,
@@ -248,35 +268,64 @@ impl Room {
})
}
+ pub(crate) fn join_channel(
+ channel_id: u64,
+ client: Arc,
+ user_store: ModelHandle,
+ cx: &mut AppContext,
+ ) -> Task>> {
+ cx.spawn(|cx| async move {
+ Self::from_join_response(
+ client.request(proto::JoinChannel { channel_id }).await?,
+ client,
+ user_store,
+ cx,
+ )
+ })
+ }
+
pub(crate) fn join(
call: &IncomingCall,
client: Arc,
user_store: ModelHandle,
cx: &mut AppContext,
) -> Task>> {
- let room_id = call.room_id;
- cx.spawn(|mut cx| async move {
- let response = client.request(proto::JoinRoom { id: room_id }).await?;
- let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
- let room = cx.add_model(|cx| {
- Self::new(
- room_id,
- response.live_kit_connection_info,
- client,
- user_store,
- cx,
- )
- });
- room.update(&mut cx, |room, cx| {
- room.leave_when_empty = true;
- room.apply_room_update(room_proto, cx)?;
- anyhow::Ok(())
- })?;
-
- Ok(room)
+ let id = call.room_id;
+ cx.spawn(|cx| async move {
+ Self::from_join_response(
+ client.request(proto::JoinRoom { id }).await?,
+ client,
+ user_store,
+ cx,
+ )
})
}
+ fn from_join_response(
+ response: proto::JoinRoomResponse,
+ client: Arc,
+ user_store: ModelHandle,
+ mut cx: AsyncAppContext,
+ ) -> Result> {
+ let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+ let room = cx.add_model(|cx| {
+ Self::new(
+ room_proto.id,
+ response.channel_id,
+ response.live_kit_connection_info,
+ client,
+ user_store,
+ cx,
+ )
+ });
+ room.update(&mut cx, |room, cx| {
+ room.leave_when_empty = room.channel_id.is_none();
+ room.apply_room_update(room_proto, cx)?;
+ anyhow::Ok(())
+ })?;
+ Ok(room)
+ }
+
fn should_leave(&self) -> bool {
self.leave_when_empty
&& self.pending_room_update.is_none()
@@ -297,7 +346,18 @@ impl Room {
}
log::info!("leaving room");
+ Audio::play_sound(Sound::Leave, cx);
+ self.clear_state(cx);
+
+ let leave_room = self.client.request(proto::LeaveRoom {});
+ cx.background().spawn(async move {
+ leave_room.await?;
+ anyhow::Ok(())
+ })
+ }
+
+ pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
@@ -314,8 +374,6 @@ impl Room {
}
}
- Audio::play_sound(Sound::Leave, cx);
-
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
@@ -324,12 +382,6 @@ impl Room {
self.live_kit.take();
self.pending_room_update.take();
self.maintain_connection.take();
-
- let leave_room = self.client.request(proto::LeaveRoom {});
- cx.background().spawn(async move {
- leave_room.await?;
- anyhow::Ok(())
- })
}
async fn maintain_connection(
@@ -1066,11 +1118,11 @@ impl Room {
})
}
- pub fn is_muted(&self) -> bool {
+ pub fn is_muted(&self, cx: &AppContext) -> bool {
self.live_kit
.as_ref()
.and_then(|live_kit| match &live_kit.microphone_track {
- LocalTrack::None => Some(true),
+ LocalTrack::None => Some(settings::get::(cx).mute_on_join),
LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted),
})
@@ -1260,7 +1312,7 @@ impl Room {
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> {
- let should_mute = !self.is_muted();
+ let should_mute = !self.is_muted(cx);
if let Some(live_kit) = self.live_kit.as_mut() {
if matches!(live_kit.microphone_track, LocalTrack::None) {
return Ok(self.share_microphone(cx));
diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs
new file mode 100644
index 0000000000..03d334a9de
--- /dev/null
+++ b/crates/client/src/channel_store.rs
@@ -0,0 +1,550 @@
+use crate::Status;
+use crate::{Client, Subscription, User, UserStore};
+use anyhow::anyhow;
+use anyhow::Result;
+use collections::HashMap;
+use collections::HashSet;
+use futures::channel::mpsc;
+use futures::Future;
+use futures::StreamExt;
+use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use rpc::{proto, TypedEnvelope};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub type ChannelId = u64;
+pub type UserId = u64;
+
+pub struct ChannelStore {
+ channels_by_id: HashMap>,
+ channel_paths: Vec>,
+ channel_invitations: Vec>,
+ channel_participants: HashMap>>,
+ channels_with_admin_privileges: HashSet,
+ outgoing_invites: HashSet<(ChannelId, UserId)>,
+ update_channels_tx: mpsc::UnboundedSender,
+ client: Arc,
+ user_store: ModelHandle,
+ _rpc_subscription: Subscription,
+ _watch_connection_status: Task<()>,
+ _update_channels: Task<()>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Channel {
+ pub id: ChannelId,
+ pub name: String,
+}
+
+pub struct ChannelMembership {
+ pub user: Arc,
+ pub kind: proto::channel_member::Kind,
+ pub admin: bool,
+}
+
+pub enum ChannelEvent {
+ ChannelCreated(ChannelId),
+ ChannelRenamed(ChannelId),
+}
+
+impl Entity for ChannelStore {
+ type Event = ChannelEvent;
+}
+
+pub enum ChannelMemberStatus {
+ Invited,
+ Member,
+ NotMember,
+}
+
+impl ChannelStore {
+ pub fn new(
+ client: Arc,
+ user_store: ModelHandle,
+ cx: &mut ModelContext,
+ ) -> Self {
+ let rpc_subscription =
+ client.add_message_handler(cx.handle(), Self::handle_update_channels);
+
+ let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
+ let mut connection_status = client.status();
+ let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
+ while let Some(status) = connection_status.next().await {
+ if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.channels_by_id.clear();
+ this.channel_invitations.clear();
+ this.channel_participants.clear();
+ this.channels_with_admin_privileges.clear();
+ this.channel_paths.clear();
+ this.outgoing_invites.clear();
+ cx.notify();
+ });
+ } else {
+ break;
+ }
+ }
+ }
+ });
+ Self {
+ channels_by_id: HashMap::default(),
+ channel_invitations: Vec::default(),
+ channel_paths: Vec::default(),
+ channel_participants: Default::default(),
+ channels_with_admin_privileges: Default::default(),
+ outgoing_invites: Default::default(),
+ update_channels_tx,
+ client,
+ user_store,
+ _rpc_subscription: rpc_subscription,
+ _watch_connection_status: watch_connection_status,
+ _update_channels: cx.spawn_weak(|this, mut cx| async move {
+ while let Some(update_channels) = update_channels_rx.next().await {
+ if let Some(this) = this.upgrade(&cx) {
+ let update_task = this.update(&mut cx, |this, cx| {
+ this.update_channels(update_channels, cx)
+ });
+ if let Some(update_task) = update_task {
+ update_task.await.log_err();
+ }
+ }
+ }
+ }),
+ }
+ }
+
+ pub fn channel_count(&self) -> usize {
+ self.channel_paths.len()
+ }
+
+ pub fn channels(&self) -> impl '_ + Iterator- )> {
+ self.channel_paths.iter().map(move |path| {
+ let id = path.last().unwrap();
+ let channel = self.channel_for_id(*id).unwrap();
+ (path.len() - 1, channel)
+ })
+ }
+
+ pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> {
+ let path = self.channel_paths.get(ix)?;
+ let id = path.last().unwrap();
+ let channel = self.channel_for_id(*id).unwrap();
+ Some((path.len() - 1, channel))
+ }
+
+ pub fn channel_invitations(&self) -> &[Arc] {
+ &self.channel_invitations
+ }
+
+ pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> {
+ self.channels_by_id.get(&channel_id)
+ }
+
+ pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
+ self.channel_paths.iter().any(|path| {
+ if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+ path[..=ix]
+ .iter()
+ .any(|id| self.channels_with_admin_privileges.contains(id))
+ } else {
+ false
+ }
+ })
+ }
+
+ pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] {
+ self.channel_participants
+ .get(&channel_id)
+ .map_or(&[], |v| v.as_slice())
+ }
+
+ pub fn create_channel(
+ &self,
+ name: &str,
+ parent_id: Option,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ let client = self.client.clone();
+ let name = name.trim_start_matches("#").to_owned();
+ cx.spawn(|this, mut cx| async move {
+ let channel = client
+ .request(proto::CreateChannel { name, parent_id })
+ .await?
+ .channel
+ .ok_or_else(|| anyhow!("missing channel in response"))?;
+
+ let channel_id = channel.id;
+
+ this.update(&mut cx, |this, cx| {
+ let task = this.update_channels(
+ proto::UpdateChannels {
+ channels: vec![channel],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert!(task.is_none());
+
+ // This event is emitted because the collab panel wants to clear the pending edit state
+ // before this frame is rendered. But we can't guarantee that the collab panel's future
+ // will resolve before this flush_effects finishes. Synchronously emitting this event
+ // ensures that the collab panel will observe this creation before the frame completes
+ cx.emit(ChannelEvent::ChannelCreated(channel_id));
+ });
+
+ Ok(channel_id)
+ })
+ }
+
+ pub fn invite_member(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ admin: bool,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("invite request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::InviteChannelMember {
+ channel_id,
+ user_id,
+ admin,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+
+ result?;
+
+ Ok(())
+ })
+ }
+
+ pub fn remove_member(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: u64,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("invite request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::RemoveChannelMember {
+ channel_id,
+ user_id,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+ result?;
+ Ok(())
+ })
+ }
+
+ pub fn set_member_admin(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ admin: bool,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("member request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::SetChannelMemberAdmin {
+ channel_id,
+ user_id,
+ admin,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+
+ result?;
+ Ok(())
+ })
+ }
+
+ pub fn rename(
+ &mut self,
+ channel_id: ChannelId,
+ new_name: &str,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ let client = self.client.clone();
+ let name = new_name.to_string();
+ cx.spawn(|this, mut cx| async move {
+ let channel = client
+ .request(proto::RenameChannel { channel_id, name })
+ .await?
+ .channel
+ .ok_or_else(|| anyhow!("missing channel in response"))?;
+ this.update(&mut cx, |this, cx| {
+ let task = this.update_channels(
+ proto::UpdateChannels {
+ channels: vec![channel],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert!(task.is_none());
+
+ // This event is emitted because the collab panel wants to clear the pending edit state
+ // before this frame is rendered. But we can't guarantee that the collab panel's future
+ // will resolve before this flush_effects finishes. Synchronously emitting this event
+ // ensures that the collab panel will observe this creation before the frame complete
+ cx.emit(ChannelEvent::ChannelRenamed(channel_id))
+ });
+ Ok(())
+ })
+ }
+
+ pub fn respond_to_channel_invite(
+ &mut self,
+ channel_id: ChannelId,
+ accept: bool,
+ ) -> impl Future