Merge branch 'main' into project_search_design

This commit is contained in:
Mikayla 2023-08-17 01:56:05 -07:00
commit afebe3faf8
No known key found for this signature in database
133 changed files with 9714 additions and 2998 deletions

267
Cargo.lock generated
View file

@ -126,18 +126,17 @@ dependencies = [
[[package]] [[package]]
name = "alacritty_config" name = "alacritty_config"
version = "0.1.2-dev" 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 = [ dependencies = [
"log", "log",
"serde", "serde",
"toml 0.7.6", "toml 0.7.6",
"winit",
] ]
[[package]] [[package]]
name = "alacritty_config_derive" name = "alacritty_config_derive"
version = "0.2.2-dev" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -147,7 +146,7 @@ dependencies = [
[[package]] [[package]]
name = "alacritty_terminal" name = "alacritty_terminal"
version = "0.20.0-dev" 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 = [ dependencies = [
"alacritty_config", "alacritty_config",
"alacritty_config_derive", "alacritty_config_derive",
@ -213,30 +212,6 @@ version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049" 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]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -926,25 +901,6 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "blocking" name = "blocking"
version = "1.3.1" version = "1.3.1"
@ -1126,20 +1082,6 @@ dependencies = [
"util", "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]] [[package]]
name = "cap-fs-ext" name = "cap-fs-ext"
version = "0.24.4" version = "0.24.4"
@ -1248,12 +1190,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.26" version = "0.4.26"
@ -1479,7 +1415,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.16.0" version = "0.17.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-tungstenite", "async-tungstenite",
@ -1552,6 +1488,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"context_menu", "context_menu",
"db",
"editor", "editor",
"feedback", "feedback",
"futures 0.3.28", "futures 0.3.28",
@ -1563,9 +1500,11 @@ dependencies = [
"postage", "postage",
"project", "project",
"recent_projects", "recent_projects",
"schemars",
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
"staff_mode",
"theme", "theme",
"theme_selector", "theme_selector",
"util", "util",
@ -2070,15 +2009,6 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.5.0" version = "5.5.0"
@ -2285,12 +2215,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.2" version = "0.5.2"
@ -4530,7 +4454,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"jni-sys", "jni-sys",
"ndk-sys", "ndk-sys",
"num_enum 0.5.11", "num_enum",
"raw-window-handle", "raw-window-handle",
"thiserror", "thiserror",
] ]
@ -4572,19 +4496,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "nix" name = "nix"
version = "0.26.2" version = "0.26.2"
@ -4750,16 +4661,7 @@ version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [ dependencies = [
"num_enum_derive 0.5.11", "num_enum_derive",
]
[[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",
] ]
[[package]] [[package]]
@ -4774,18 +4676,6 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "nvim-rs" name = "nvim-rs"
version = "0.5.0" version = "0.5.0"
@ -4811,32 +4701,6 @@ dependencies = [
"objc_exception", "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]] [[package]]
name = "objc_exception" name = "objc_exception"
version = "0.1.2" version = "0.1.2"
@ -4955,15 +4819,6 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "2.10.0" version = "2.10.0"
@ -5711,6 +5566,17 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quick_action_bar"
version = "0.1.0"
dependencies = [
"editor",
"gpui",
"search",
"theme",
"workspace",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.32" version = "1.0.32"
@ -7092,15 +6958,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" 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]] [[package]]
name = "sluice" name = "sluice"
version = "0.5.5" version = "0.5.5"
@ -7145,15 +7002,6 @@ dependencies = [
"pin-project-lite 0.1.12", "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]] [[package]]
name = "snippet" name = "snippet"
version = "0.1.0" version = "0.1.0"
@ -8840,12 +8688,6 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -9310,17 +9152,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "webpki" name = "webpki"
version = "0.21.4" version = "0.21.4"
@ -9636,42 +9467,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 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]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.2" version = "0.5.2"
@ -9789,25 +9584,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "xmlparser" name = "xmlparser"
version = "0.13.5" version = "0.13.5"
@ -9860,7 +9636,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.100.0" version = "0.101.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
@ -9919,6 +9695,7 @@ dependencies = [
"project", "project",
"project_panel", "project_panel",
"project_symbols", "project_symbols",
"quick_action_bar",
"rand 0.8.5", "rand 0.8.5",
"recent_projects", "recent_projects",
"regex", "regex",

23
assets/icons/ai.svg Normal file
View file

@ -0,0 +1,23 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8.94203V11C7.38649 11 6.61351 11 4 11V10.6812L10 5.31884V5H4V7.08696" stroke="black" stroke-width="1.25"/>
<circle cx="0.5" cy="8" r="0.5" fill="black"/>
<circle cx="1.49976" cy="5.82825" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="1.49976" cy="10.1719" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="13.5" cy="8.01581" r="0.5" fill="black"/>
<circle cx="12.5" cy="5.84387" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="12.5" cy="10.1877" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="6.99219" cy="1.48438" r="0.5" fill="black"/>
<circle cx="4.5" cy="2.5" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="0.5" cy="12.016" r="0.5" fill="black"/>
<circle cx="0.5" cy="3.98438" r="0.5" fill="black"/>
<circle cx="13.5" cy="12.016" r="0.5" fill="black"/>
<circle cx="13.5" cy="3.98438" r="0.5" fill="black"/>
<circle cx="2.49976" cy="14.516" r="0.5" fill="black"/>
<circle cx="2.48413" cy="1.48438" r="0.5" fill="black"/>
<circle cx="11.5" cy="14.516" r="0.5" fill="black"/>
<circle cx="11.5" cy="1.48438" r="0.5" fill="black"/>
<circle cx="9.49609" cy="2.48438" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="6.99219" cy="14.5" r="0.5" fill="black"/>
<circle cx="4.50391" cy="13.516" r="0.5" fill="black" fill-opacity="0.75"/>
<circle cx="9.49609" cy="13.5" r="0.5" fill="black" fill-opacity="0.75"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.125 6.99344L6.35938 3.63281M3.125 6.99344L6.35938 10.3672M3.125 6.99344H11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8906 7.00125L7.64062 3.64062M10.8906 7.00125L7.64062 10.375M10.8906 7.00125H3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

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

After

Width:  |  Height:  |  Size: 571 B

3
assets/icons/check.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.98438 7.85115L6.13569 9.44983L9.98438 4.08141" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8L6.5 9L9 5.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="7" cy="7" r="4.875" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.63281 5.66406L6.99344 8.89844L10.3672 5.66406" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.35938 3.63281L5.125 6.99344L8.35938 10.3672" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.64062 3.64062L8.89062 7.00125L5.64062 10.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 245 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.63281 8.36719L6.99344 5.13281L10.3672 8.36719" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46115 8.43419C7.30678 8.43419 8.92229 7.43411 8.92229 5.21171C8.92229 2.98933 7.30678 1.98926 5.46115 1.98926C3.61553 1.98926 2 2.98933 2 5.21171C2 6.028 2.21794 6.67935 2.58519 7.17685C2.7184 7.35732 2.69033 7.77795 2.58387 7.97539C2.32908 8.44793 2.81048 8.9657 3.33372 8.84571C3.72539 8.75597 4.13621 8.63447 4.49574 8.4715C4.62736 8.41181 4.7727 8.38777 4.91631 8.40402C5.09471 8.42416 5.27678 8.43419 5.46115 8.43419Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4661 10.6353C10.4697 10.7833 10.4914 10.9562 10.5423 11.1245C10.2394 11.0477 9.94714 10.9535 9.69757 10.8403C9.44717 10.7269 9.1668 10.6793 8.88556 10.7111C8.73612 10.728 8.58194 10.7365 8.42443 10.7365C7.68587 10.7365 7.04509 10.5503 6.58359 10.213C6.25127 9.97033 5.78501 10.0428 5.54218 10.3751C5.29939 10.7075 5.37193 11.1737 5.70427 11.4165C6.48017 11.9834 7.45185 12.2271 8.42443 12.2271C8.6356 12.2271 8.84564 12.2156 9.05296 12.1921C9.05904 12.1914 9.06942 12.1921 9.08212 12.1979C9.50348 12.3888 9.9667 12.5238 10.3854 12.6198C10.933 12.7453 11.4558 12.536 11.7761 12.1748C11.9716 11.9544 12.0298 11.6167 12.043 11.361C12.0564 11.1006 12.0238 10.8609 11.9375 10.6152C12.3875 9.98145 12.6308 9.18769 12.6308 8.2593C12.6308 7.23782 12.3361 6.3809 11.7994 5.72187C11.5395 5.4027 11.07 5.35466 10.7509 5.61459C10.4318 5.87448 10.3837 6.34387 10.6436 6.66305M10.4661 10.6353C10.4612 10.4326 10.4844 10.075 10.7008 9.78189C10.9613 9.42893 11.1403 8.93793 11.1403 8.2593C11.1403 7.53473 10.9364 7.0226 10.6436 6.66305" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

9
assets/icons/copilot.svg Normal file
View file

@ -0,0 +1,9 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.64063 7.67017C5.97718 7.67017 6.25 7.94437 6.25 8.28263V9.60963C6.25 9.94786 5.97718 10.2221 5.64063 10.2221C5.30408 10.2221 5.03125 9.94786 5.03125 9.60963V8.28263C5.03125 7.94437 5.30408 7.67017 5.64063 7.67017Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.37537 7.67017C8.71192 7.67017 8.98474 7.94437 8.98474 8.28263V9.60963C8.98474 9.94786 8.71192 10.2221 8.37537 10.2221C8.03882 10.2221 7.76599 9.94786 7.76599 9.60963V8.28263C7.76599 7.94437 8.03882 7.67017 8.37537 7.67017Z" fill="black"/>
<path d="M7 3.65625C7 5.84375 5.10754 6.3718 3.76562 6.3718C2.42371 6.3718 2.1405 5.3854 2.1405 4.16861C2.1405 2.95182 3.22834 1.96542 4.57025 1.96542C5.91216 1.96542 7 2.43946 7 3.65625Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25"/>
<path d="M7 3.65625C7 5.84375 8.89246 6.3718 10.2344 6.3718C11.5763 6.3718 11.8595 5.3854 11.8595 4.16861C11.8595 2.95182 10.7717 1.96542 9.42975 1.96542C8.08784 1.96542 7 2.43946 7 3.65625Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25"/>
<path d="M11.0156 6.01562C11.0156 6.01562 11.6735 6.43636 12 7.07348C12.3265 7.7106 12.3281 9.18621 12 9.7181C11.6719 10.25 11.2813 10.625 10.2931 11.16C9.30501 11.695 8 12.0156 8 12.0156H6C6 12.0156 4.70312 11.7344 3.70687 11.16C2.71061 10.5856 2.23437 10.2188 2 9.7181C1.76562 9.21746 1.6875 7.75 2 7.07348C2.31249 6.39695 3 6.01562 3 6.01562" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M10.4454 11.0264V6.41934L12.1671 6.99323V9.5598L10.4454 11.0264Z" fill="black" fill-opacity="0.75"/>
<path d="M3.51556 11.0264V6.41934L1.79388 6.99323V9.5598L3.51556 11.0264Z" fill="black" fill-opacity="0.75"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

5
assets/icons/copy.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="5.64062" width="6.35938" height="6.35938" rx="0.5" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M8.01562 3.75H5.625V2.03125H11.9375V8.39062H10.2656V6C10.2656 4.75736 9.25827 3.75 8.01562 3.75Z" fill="black" fill-opacity="0.5"/>
<path d="M5.625 3.125V2.5C5.625 2.22386 5.84886 2 6.125 2H11.5C11.7761 2 12 2.22386 12 2.5V7.875C12 8.15114 11.7761 8.375 11.5 8.375H10.8906" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="7" cy="7" r="1" fill="black"/>
<circle cx="11" cy="7" r="1" fill="black"/>
<circle cx="3" cy="7" r="1" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

4
assets/icons/error.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 745 B

4
assets/icons/exit.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3594 7.00127L9.86062 4.5025M12.3594 7.00127L9.86062 9.50002M12.3594 7.00127L5 7.00127" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H6" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3.5C2 3.22386 2.22386 3 2.5 3H11.5C11.7761 3 12 3.22386 12 3.5V10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V3.5Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M3 4L6.95312 7L11 4" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 9L5 8" stroke="black" stroke-opacity="0.5" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9L9 8" stroke="black" stroke-opacity="0.5" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 675 B

3
assets/icons/filter.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6749 2.40608C11.8058 2.24239 11.6893 1.99991 11.4796 1.99991H2.51996C2.31033 1.99991 2.19379 2.24239 2.32474 2.40608L5.14583 5.93246C5.34148 6.17701 5.44808 6.48087 5.44808 6.79412C5.44808 7.46881 5.44808 10.334 5.44808 11.5016C5.44808 11.7778 5.67194 11.9999 5.94808 11.9999H8.05153C8.32767 11.9999 8.55153 11.7778 8.55153 11.5016C8.55153 10.334 8.55153 7.46881 8.55153 6.79412C8.55153 6.48087 8.65815 6.17701 8.8538 5.93246L11.6749 2.40608Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 644 B

6
assets/icons/hash.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 573 B

5
assets/icons/html.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.15735 3.17108L5.84271 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3" cy="9" r="1" fill="black"/>
<circle cx="3" cy="5" r="1" fill="black"/>
<path d="M7 3H10M13 3H10M10 3C10 3 10 11 10 11.5" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

5
assets/icons/kebab.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="7" cy="7" r="1" fill="black"/>
<circle cx="11" cy="7" r="1" fill="black"/>
<circle cx="3" cy="7" r="1" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

6
assets/icons/lock.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 445 B

View file

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

After

Width:  |  Height:  |  Size: 383 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.47087 3.20502H4.93146L7.12233 10.845H6.16733L5.60557 8.91252H2.78552L2.235 10.845H1.28L3.47087 3.20502ZM5.3921 8.06988L4.24611 4.02519H4.15622L3.01023 8.06988H5.3921Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.35784 3.05502H5.04449L7.32139 10.995H6.05473L5.49297 9.06253H2.89876L2.34823 10.995H1.08094L3.35784 3.05502ZM4.20117 4.41683L3.20863 7.91989H5.1937L4.20117 4.41683Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.49755 10.9439L8.49614 10.9433C8.20513 10.8235 7.99172 10.6192 7.86261 10.3352L7.86103 10.3317C7.74397 10.0558 7.69085 9.67797 7.69085 9.21014C7.69085 8.65676 7.77089 8.20993 7.94588 7.88453C8.12486 7.54406 8.4223 7.31025 8.82246 7.17939C9.22218 7.04366 9.77245 6.97946 10.4643 6.97946H11.3773V6.84676C11.3773 6.53978 11.3365 6.32064 11.2676 6.17645L11.2652 6.17158C11.2077 6.03931 11.105 5.94128 10.942 5.87857L10.9401 5.87785C10.7779 5.81296 10.5289 5.77548 10.1816 5.77548C9.95742 5.77548 9.77444 5.79025 9.63048 5.818C9.4849 5.84607 9.38928 5.88554 9.33128 5.92772L9.32759 5.9304C9.22055 6.00339 9.13583 6.16518 9.1215 6.4804L9.11499 6.62359H7.87178V6.47359C7.87178 6.02598 7.93666 5.66152 8.08202 5.39592C8.23181 5.11455 8.48509 4.9233 8.82297 4.81582C9.15491 4.7028 9.61083 4.64999 10.1816 4.64999C10.7762 4.64999 11.2497 4.71047 11.5915 4.84054C11.9497 4.97397 12.2081 5.20795 12.3539 5.54148C12.5023 5.86386 12.5706 6.30304 12.5706 6.84676V10.9998H11.4112V10.4612C11.2513 10.6622 11.0717 10.8156 10.8706 10.9161L10.869 10.917C10.5893 11.0526 10.1848 11.1129 9.67276 11.1129C9.18264 11.1129 8.78731 11.0598 8.49755 10.9439ZM9.40357 8.21033C9.23125 8.26777 9.11727 8.36187 9.04741 8.4893C8.98131 8.61621 8.94073 8.81734 8.94073 9.10837C8.94073 9.48881 9.01919 9.69954 9.12735 9.79866C9.24209 9.89577 9.48642 9.96479 9.91023 9.96479C10.3198 9.96479 10.6134 9.90216 10.8072 9.79296C10.9944 9.68003 11.1366 9.4918 11.226 9.21004C11.3088 8.94889 11.3567 8.57563 11.3648 8.08368L10.2403 8.09363C9.87055 8.10107 9.59539 8.14186 9.40658 8.20929L9.40357 8.21033Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.74677 9.48683L4.07035 6.03229L3.38589 9.48683H2.17618L1.00285 4.00778H2.27563L2.81571 7.41751L3.48443 4.01749H4.65869L5.31824 7.41173L5.8574 4.00778H7.13018L5.95684 9.48683H4.74677Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64902 9.324C8.31295 9.13305 8.08208 8.81972 7.9472 8.40798C7.81465 8.00336 7.75312 7.44225 7.75312 6.73695C7.75312 6.03863 7.81136 5.48473 7.93685 5.08734L7.9375 5.0853C8.07226 4.67391 8.30335 4.36276 8.64083 4.17813C8.96801 3.99275 9.41114 3.91059 9.94955 3.91059C10.3406 3.91059 10.6631 3.95604 10.8967 4.06503C11.0079 4.11693 11.1098 4.18862 11.2033 4.27763V2.03046H12.4076V9.48579H11.2033V9.19001C11.0944 9.29092 10.9799 9.37114 10.8591 9.4277C10.6327 9.53666 10.3334 9.5827 9.97862 9.5827C9.43385 9.5827 8.98587 9.50374 8.6537 9.32658L8.64902 9.324ZM11.1139 7.85526C11.1841 7.60329 11.2226 7.23372 11.2226 6.73695C11.2226 6.2462 11.184 5.88349 11.114 5.63862C11.0456 5.39921 10.94 5.25882 10.8149 5.18284L10.8077 5.17844C10.6804 5.09361 10.4713 5.03744 10.1531 5.03744C9.8078 5.03744 9.57185 5.09378 9.42251 5.18338L9.41824 5.18594C9.27997 5.2643 9.16717 5.40621 9.09394 5.64281C9.01872 5.88584 8.97689 6.24686 8.97689 6.73695C8.97689 7.23381 9.01877 7.59792 9.09394 7.84078C9.16725 8.07763 9.28092 8.22495 9.42251 8.3099C9.57185 8.39951 9.8078 8.45585 10.1531 8.45585C10.4721 8.45585 10.683 8.40283 10.8113 8.32234C10.9395 8.23962 11.0456 8.09391 11.1139 7.85526Z" fill="black"/>
<rect x="1.14084" y="10.7188" width="11.7183" height="1.26565" rx="0.632824" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8.5V12M2 12H5.5M2 12L6.01562 7.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5.5V2M12 2L8.5 2M12 2L8.01562 5.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 8.5C10.5 8.5 9.375 10 7 10C4.625 10 3.5 8.5 3.5 8.5" stroke="black" stroke-width="1.25"/>
<rect x="5" y="2" width="4" height="5.40625" rx="2" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
<path d="M7 10V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.01563 11.4844L6.01563 7.98438M6.01563 7.98438L2.51563 7.98437M6.01563 7.98438L2 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.01562 2.48438V5.98438M8.01562 5.98438H11.5156M8.01562 5.98438L12 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

3
assets/icons/plus.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 188 B

5
assets/icons/project.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 588 B

11
assets/icons/replace.svg Normal file
View file

@ -0,0 +1,11 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 12C4.97279 12 3.22735 10.7936 2.4425 9.0595M7 2C9.11228 2 10.9186 3.30981 11.6512 5.16152" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="1.65625" cy="1.67188" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="3.71094" cy="1.67188" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="4.96094" cy="3.36719" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="3.71094" cy="4.79688" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="4.60156" cy="6.67188" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="1.65625" cy="4.17188" r="0.625" fill="black" fill-opacity="0.75"/>
<circle cx="1.65625" cy="6.67188" r="0.625" fill="black" fill-opacity="0.75"/>
<path d="M10.7802 10.8195C10.838 10.8195 10.8906 10.8527 10.9155 10.9048L11.7174 12.5811C11.8088 12.7721 12.0017 12.8938 12.2135 12.8938H12.3394C12.7483 12.8938 13.0142 12.4635 12.8314 12.0978L12.1619 10.7589C12.1232 10.6816 12.1582 10.5823 12.241 10.5349C12.7565 10.2397 13.0695 9.66858 13.0695 9.00391C13.0695 8.43361 12.8777 7.97006 12.5248 7.64951C12.1725 7.3295 11.6652 7.15703 11.043 7.15703H9.49609C9.19234 7.15703 8.94609 7.40327 8.94609 7.70703V12.3438C8.94609 12.6475 9.19234 12.8938 9.49609 12.8938H9.60156C9.90532 12.8938 10.1516 12.6475 10.1516 12.3438V10.9695C10.1516 10.8867 10.2187 10.8195 10.3016 10.8195H10.7802ZM10.1516 8.31328C10.1516 8.23044 10.2187 8.16328 10.3016 8.16328H10.8984C11.2023 8.16328 11.4371 8.2449 11.5954 8.38814C11.7529 8.5308 11.8406 8.73993 11.8406 9.00781C11.8406 9.28155 11.751 9.49461 11.5909 9.63971C11.4302 9.7854 11.1925 9.86797 10.8867 9.86797H10.3016C10.2187 9.86797 10.1516 9.80081 10.1516 9.71797V8.31328Z" fill="black" stroke="black" stroke-width="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.10517 5.8012C4.07193 5.73172 4.00176 5.6875 3.92475 5.6875H3.44609C3.33564 5.6875 3.24609 5.77704 3.24609 5.8875V7.26172C3.24609 7.53786 3.02224 7.76172 2.74609 7.76172H2.64062C2.36448 7.76172 2.14062 7.53786 2.14062 7.26172V2.625C2.14062 2.34886 2.36448 2.125 2.64062 2.125H4.1875C5.41406 2.125 6.16406 2.80469 6.16406 3.92188C6.16406 4.57081 5.85885 5.12418 5.36073 5.40943C5.25888 5.46775 5.20921 5.59421 5.2617 5.69918L5.93117 7.03811C6.09739 7.37056 5.85564 7.76172 5.48395 7.76172H5.35806C5.16552 7.76172 4.99009 7.65117 4.907 7.47748L4.10517 5.8012ZM3.44609 3.03125C3.33564 3.03125 3.24609 3.12079 3.24609 3.23125V4.63594C3.24609 4.74639 3.33564 4.83594 3.44609 4.83594H4.03125C4.66016 4.83594 5.03516 4.49609 5.03516 3.92578C5.03516 3.36719 4.66797 3.03125 4.04297 3.03125H3.44609Z" fill="black" fill-opacity="0.75"/>
<path d="M3.92475 5.7375C3.98251 5.7375 4.03514 5.77067 4.06006 5.82277L4.8619 7.49905C4.95329 7.69011 5.14627 7.81172 5.35806 7.81172H5.48395C5.89281 7.81172 6.15873 7.38145 5.97589 7.01575L5.30642 5.67682C5.26778 5.59953 5.30269 5.50028 5.38557 5.45282C5.90107 5.15762 6.21406 4.58655 6.21406 3.92188C6.21406 3.35158 6.02226 2.88803 5.66936 2.56748C5.31705 2.24747 4.80973 2.075 4.1875 2.075H2.64062C2.33687 2.075 2.09062 2.32124 2.09062 2.625V7.26172C2.09062 7.56548 2.33687 7.81172 2.64062 7.81172H2.74609C3.04985 7.81172 3.29609 7.56548 3.29609 7.26172V5.8875C3.29609 5.80466 3.36325 5.7375 3.44609 5.7375H3.92475ZM3.29609 3.23125C3.29609 3.14841 3.36325 3.08125 3.44609 3.08125H4.04297C4.34688 3.08125 4.58164 3.16287 4.73988 3.30611C4.89748 3.44876 4.98516 3.6579 4.98516 3.92578C4.98516 4.19952 4.89553 4.41258 4.73546 4.55768C4.57475 4.70337 4.33706 4.78594 4.03125 4.78594H3.44609C3.36325 4.78594 3.29609 4.71878 3.29609 4.63594V3.23125Z" stroke="black" stroke-opacity="0.75" stroke-width="0.1"/>
<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

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

After

Width:  |  Height:  |  Size: 2 KiB

4
assets/icons/screen.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="10" height="7" rx="0.5" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
<path d="M7 9V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

5
assets/icons/split.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 2H10C11.1046 2 12 2.89543 12 4V10C12 11.1046 11.1046 12 10 12H7V2Z" fill="black" fill-opacity="0.25"/>
<rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/>
<line x1="7" y1="2" x2="7" y2="12" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

4
assets/icons/success.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2.5C2 2.22386 2.22386 2 2.5 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H2.5C2.22386 12 2 11.7761 2 11.5V2.5Z" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M4.60938 7.625L6.3125 8.89062L9.35938 4.64062" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.65625 2.5C1.65625 2.22386 1.88011 2 2.15625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V11.5C12.3438 11.7761 12.1199 12 11.8437 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V2.5Z" stroke="black" stroke-width="1.25"/>
<path d="M4.375 9L6.375 7L4.375 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.625 9L9.90625 9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 549 B

5
assets/icons/warning.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 7L7 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="7" cy="9.24219" r="0.75" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

3
assets/icons/x.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.82843 4.17157L4.17157 9.82842M9.82843 9.82842L4.17157 4.17157" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View file

@ -13,6 +13,7 @@
"cmd-up": "menu::SelectFirst", "cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast", "cmd-down": "menu::SelectLast",
"enter": "menu::Confirm", "enter": "menu::Confirm",
"ctrl-enter": "menu::ShowContextMenu",
"cmd-enter": "menu::SecondaryConfirm", "cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel", "escape": "menu::Cancel",
"ctrl-c": "menu::Cancel", "ctrl-c": "menu::Cancel",
@ -517,7 +518,8 @@
{ {
"bindings": { "bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", "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" "cmd-alt-i": "zed::DebugElements"
} }
}, },
@ -553,6 +555,25 @@
"alt-shift-f": "project_panel::NewSearchInDirectory" "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", "context": "Terminal",
"bindings": { "bindings": {

View file

@ -122,7 +122,17 @@
// Amount of indentation for nested items. // Amount of indentation for nested items.
"indent_size": 20 "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": { "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'. // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right", "dock": "right",
// Default width when the assistant is docked to the left or right. // Default width when the assistant is docked to the left or right.

View file

@ -192,6 +192,7 @@ impl AssistantPanel {
old_dock_position = new_dock_position; old_dock_position = new_dock_position;
cx.emit(AssistantPanelEvent::DockPositionChanged); cx.emit(AssistantPanelEvent::DockPositionChanged);
} }
cx.notify();
})]; })];
this this
@ -725,10 +726,10 @@ impl Panel for AssistantPanel {
} }
} }
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) { fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
match self.position(cx) { match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = Some(size), DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = Some(size), DockPosition::Bottom => self.height = size,
} }
cx.notify(); cx.notify();
} }
@ -780,8 +781,10 @@ impl Panel for AssistantPanel {
} }
} }
fn icon_path(&self) -> &'static str { fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
"icons/robot_14.svg" settings::get::<AssistantSettings>(cx)
.button
.then(|| "icons/ai.svg")
} }
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) { fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

View file

@ -13,6 +13,7 @@ pub enum AssistantDockPosition {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct AssistantSettings { pub struct AssistantSettings {
pub button: bool,
pub dock: AssistantDockPosition, pub dock: AssistantDockPosition,
pub default_width: f32, pub default_width: f32,
pub default_height: f32, pub default_height: f32,
@ -20,6 +21,7 @@ pub struct AssistantSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent { pub struct AssistantSettingsContent {
pub button: Option<bool>,
pub dock: Option<AssistantDockPosition>, pub dock: Option<AssistantDockPosition>,
pub default_width: Option<f32>, pub default_width: Option<f32>,
pub default_height: Option<f32>, pub default_height: Option<f32>,

View file

@ -39,29 +39,43 @@ pub struct Audio {
impl Audio { impl Audio {
pub fn new() -> Self { pub fn new() -> Self {
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
Self { Self {
_output_stream, _output_stream: None,
output_handle, 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::<Self>() { if !cx.has_global::<Self>() {
return; return;
} }
let this = cx.global::<Self>(); cx.update_global::<Self, _, _>(|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::<Self>() {
return; return;
}; }
let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else { cx.update_global::<Self, _, _>(|this, _| {
return; this._output_stream.take();
}; this.output_handle.take();
});
output_handle.play_raw(source).log_err();
} }
} }

View file

@ -5,8 +5,11 @@ pub mod room;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings; 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 collections::HashSet;
use futures::{future::Shared, FutureExt}; use futures::{future::Shared, FutureExt};
use postage::watch; use postage::watch;
@ -75,6 +78,10 @@ impl ActiveCall {
} }
} }
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call( async fn handle_incoming_call(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::IncomingCall>, envelope: TypedEnvelope<proto::IncomingCall>,
@ -274,9 +281,36 @@ impl ActiveCall {
Ok(()) Ok(())
} }
pub fn join_channel(
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
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<Self>) -> Task<Result<()>> { pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify(); cx.notify();
self.report_call_event("hang up", cx); self.report_call_event("hang up", cx);
Audio::end_call(cx);
if let Some((room, _)) = self.room.take() { if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx)) room.update(cx, |room, cx| room.leave(cx))
} else { } else {

View file

@ -49,6 +49,7 @@ pub enum Event {
pub struct Room { pub struct Room {
id: u64, id: u64,
channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>, live_kit: Option<LiveKitRoom>,
status: RoomStatus, status: RoomStatus,
shared_projects: HashSet<WeakModelHandle<Project>>, shared_projects: HashSet<WeakModelHandle<Project>>,
@ -93,8 +94,25 @@ impl Entity for Room {
} }
impl Room { impl Room {
pub fn channel_id(&self) -> Option<u64> {
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( fn new(
id: u64, id: u64,
channel_id: Option<u64>,
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>, live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
@ -185,6 +203,7 @@ impl Room {
Self { Self {
id, id,
channel_id,
live_kit: live_kit_room, live_kit: live_kit_room,
status: RoomStatus::Online, status: RoomStatus::Online,
shared_projects: Default::default(), shared_projects: Default::default(),
@ -217,6 +236,7 @@ impl Room {
let room = cx.add_model(|cx| { let room = cx.add_model(|cx| {
Self::new( Self::new(
room_proto.id, room_proto.id,
None,
response.live_kit_connection_info, response.live_kit_connection_info,
client, client,
user_store, user_store,
@ -248,35 +268,64 @@ impl Room {
}) })
} }
pub(crate) fn join_channel(
channel_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut AppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|cx| async move {
Self::from_join_response(
client.request(proto::JoinChannel { channel_id }).await?,
client,
user_store,
cx,
)
})
}
pub(crate) fn join( pub(crate) fn join(
call: &IncomingCall, call: &IncomingCall,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Task<Result<ModelHandle<Self>>> { ) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id; let id = call.room_id;
cx.spawn(|mut cx| async move { cx.spawn(|cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?; Self::from_join_response(
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; client.request(proto::JoinRoom { id }).await?,
let room = cx.add_model(|cx| { client,
Self::new( user_store,
room_id, cx,
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)
}) })
} }
fn from_join_response(
response: proto::JoinRoomResponse,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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 { fn should_leave(&self) -> bool {
self.leave_when_empty self.leave_when_empty
&& self.pending_room_update.is_none() && self.pending_room_update.is_none()
@ -297,7 +346,18 @@ impl Room {
} }
log::info!("leaving 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() { for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade(cx) { if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
@ -314,8 +374,6 @@ impl Room {
} }
} }
Audio::play_sound(Sound::Leave, cx);
self.status = RoomStatus::Offline; self.status = RoomStatus::Offline;
self.remote_participants.clear(); self.remote_participants.clear();
self.pending_participants.clear(); self.pending_participants.clear();
@ -324,12 +382,6 @@ impl Room {
self.live_kit.take(); self.live_kit.take();
self.pending_room_update.take(); self.pending_room_update.take();
self.maintain_connection.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( 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 self.live_kit
.as_ref() .as_ref()
.and_then(|live_kit| match &live_kit.microphone_track { .and_then(|live_kit| match &live_kit.microphone_track {
LocalTrack::None => Some(true), LocalTrack::None => Some(settings::get::<CallSettings>(cx).mute_on_join),
LocalTrack::Pending { muted, .. } => Some(*muted), LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted), LocalTrack::Published { muted, .. } => Some(*muted),
}) })
@ -1260,7 +1312,7 @@ impl Room {
} }
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> { pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<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 let Some(live_kit) = self.live_kit.as_mut() {
if matches!(live_kit.microphone_track, LocalTrack::None) { if matches!(live_kit.microphone_track, LocalTrack::None) {
return Ok(self.share_microphone(cx)); return Ok(self.share_microphone(cx));

View file

@ -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<ChannelId, Arc<Channel>>,
channel_paths: Vec<Vec<ChannelId>>,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channels_with_admin_privileges: HashSet<ChannelId>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_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<User>,
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<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> 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<Item = (usize, &Arc<Channel>)> {
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<Channel>)> {
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<Channel>] {
&self.channel_invitations
}
pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
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<User>] {
self.channel_participants
.get(&channel_id)
.map_or(&[], |v| v.as_slice())
}
pub fn create_channel(
&self,
name: &str,
parent_id: Option<ChannelId>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ChannelId>> {
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<Self>,
) -> Task<Result<()>> {
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<Self>,
) -> Task<Result<()>> {
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<Self>,
) -> Task<Result<()>> {
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<Self>,
) -> Task<Result<()>> {
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<Output = Result<()>> {
let client = self.client.clone();
async move {
client
.request(proto::RespondToChannelInvite { channel_id, accept })
.await?;
Ok(())
}
}
pub fn get_channel_member_details(
&self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
let user_store = self.user_store.downgrade();
cx.spawn(|_, mut cx| async move {
let response = client
.request(proto::GetChannelMembers { channel_id })
.await?;
let user_ids = response.members.iter().map(|m| m.user_id).collect();
let user_store = user_store
.upgrade(&cx)
.ok_or_else(|| anyhow!("user store dropped"))?;
let users = user_store
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
.await?;
Ok(users
.into_iter()
.zip(response.members)
.filter_map(|(user, member)| {
Some(ChannelMembership {
user,
admin: member.admin,
kind: proto::channel_member::Kind::from_i32(member.kind)?,
})
})
.collect())
})
}
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
let client = self.client.clone();
async move {
client.request(proto::RemoveChannel { channel_id }).await?;
Ok(())
}
}
pub fn has_pending_channel_invite_response(&self, _: &Arc<Channel>) -> bool {
false
}
pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool {
self.outgoing_invites.contains(&(channel_id, user_id))
}
async fn handle_update_channels(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::UpdateChannels>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update_channels_tx
.unbounded_send(message.payload)
.unwrap();
});
Ok(())
}
pub(crate) fn update_channels(
&mut self,
payload: proto::UpdateChannels,
cx: &mut ModelContext<ChannelStore>,
) -> Option<Task<Result<()>>> {
if !payload.remove_channel_invitations.is_empty() {
self.channel_invitations
.retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
}
for channel in payload.channel_invitations {
match self
.channel_invitations
.binary_search_by_key(&channel.id, |c| c.id)
{
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
Err(ix) => self.channel_invitations.insert(
ix,
Arc::new(Channel {
id: channel.id,
name: channel.name,
}),
),
}
}
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
if channels_changed {
if !payload.remove_channels.is_empty() {
self.channels_by_id
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
self.channel_participants
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
self.channels_with_admin_privileges
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
}
for channel in payload.channels {
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
// FIXME: We may be missing a path for this existing channel in certain cases
let existing_channel = Arc::make_mut(existing_channel);
existing_channel.name = channel.name;
continue;
}
self.channels_by_id.insert(
channel.id,
Arc::new(Channel {
id: channel.id,
name: channel.name,
}),
);
if let Some(parent_id) = channel.parent_id {
let mut ix = 0;
while ix < self.channel_paths.len() {
let path = &self.channel_paths[ix];
if path.ends_with(&[parent_id]) {
let mut new_path = path.clone();
new_path.push(channel.id);
self.channel_paths.insert(ix + 1, new_path);
ix += 1;
}
ix += 1;
}
} else {
self.channel_paths.push(vec![channel.id]);
}
}
self.channel_paths.sort_by(|a, b| {
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
a.cmp(b)
});
self.channel_paths.dedup();
self.channel_paths.retain(|path| {
path.iter()
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
});
}
for permission in payload.channel_permissions {
if permission.is_admin {
self.channels_with_admin_privileges
.insert(permission.channel_id);
} else {
self.channels_with_admin_privileges
.remove(&permission.channel_id);
}
}
cx.notify();
if payload.channel_participants.is_empty() {
return None;
}
let mut all_user_ids = Vec::new();
let channel_participants = payload.channel_participants;
for entry in &channel_participants {
for user_id in entry.participant_user_ids.iter() {
if let Err(ix) = all_user_ids.binary_search(user_id) {
all_user_ids.insert(ix, *user_id);
}
}
}
let users = self
.user_store
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
Some(cx.spawn(|this, mut cx| async move {
let users = users.await?;
this.update(&mut cx, |this, cx| {
for entry in &channel_participants {
let mut participants: Vec<_> = entry
.participant_user_ids
.iter()
.filter_map(|user_id| {
users
.binary_search_by_key(&user_id, |user| &user.id)
.ok()
.map(|ix| users[ix].clone())
})
.collect();
participants.sort_by_key(|u| u.id);
this.channel_participants
.insert(entry.channel_id, participants);
}
cx.notify();
});
anyhow::Ok(())
}))
}
fn channel_path_sorting_key<'a>(
path: &'a [ChannelId],
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
) -> impl 'a + Iterator<Item = Option<&'a str>> {
path.iter()
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
}
}

View file

@ -0,0 +1,165 @@
use super::*;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_update_channels(cx: &mut AppContext) {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 1,
name: "b".to_string(),
parent_id: None,
},
proto::Channel {
id: 2,
name: "a".to_string(),
parent_id: None,
},
],
channel_permissions: vec![proto::ChannelPermission {
channel_id: 1,
is_admin: true,
}],
..Default::default()
},
cx,
);
assert_channels(
&channel_store,
&[
//
(0, "a".to_string(), false),
(0, "b".to_string(), true),
],
cx,
);
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 3,
name: "x".to_string(),
parent_id: Some(1),
},
proto::Channel {
id: 4,
name: "y".to_string(),
parent_id: Some(2),
},
],
..Default::default()
},
cx,
);
assert_channels(
&channel_store,
&[
(0, "a".to_string(), false),
(1, "y".to_string(), false),
(0, "b".to_string(), true),
(1, "x".to_string(), true),
],
cx,
);
}
#[gpui::test]
fn test_dangling_channel_paths(cx: &mut AppContext) {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
update_channels(
&channel_store,
proto::UpdateChannels {
channels: vec![
proto::Channel {
id: 0,
name: "a".to_string(),
parent_id: None,
},
proto::Channel {
id: 1,
name: "b".to_string(),
parent_id: Some(0),
},
proto::Channel {
id: 2,
name: "c".to_string(),
parent_id: Some(1),
},
],
channel_permissions: vec![proto::ChannelPermission {
channel_id: 0,
is_admin: true,
}],
..Default::default()
},
cx,
);
// Sanity check
assert_channels(
&channel_store,
&[
//
(0, "a".to_string(), true),
(1, "b".to_string(), true),
(2, "c".to_string(), true),
],
cx,
);
update_channels(
&channel_store,
proto::UpdateChannels {
remove_channels: vec![1, 2],
..Default::default()
},
cx,
);
// Make sure that the 1/2/3 path is gone
assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
}
fn update_channels(
channel_store: &ModelHandle<ChannelStore>,
message: proto::UpdateChannels,
cx: &mut AppContext,
) {
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
assert!(task.is_none());
}
#[track_caller]
fn assert_channels(
channel_store: &ModelHandle<ChannelStore>,
expected_channels: &[(usize, String, bool)],
cx: &AppContext,
) {
let actual = channel_store.read_with(cx, |store, _| {
store
.channels()
.map(|(depth, channel)| {
(
depth,
channel.name.to_string(),
store.is_user_admin(channel.id),
)
})
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
}

View file

@ -1,6 +1,10 @@
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
#[cfg(test)]
mod channel_store_tests;
pub mod channel_store;
pub mod telemetry; pub mod telemetry;
pub mod user; pub mod user;
@ -44,6 +48,7 @@ use util::channel::ReleaseChannel;
use util::http::HttpClient; use util::http::HttpClient;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
pub use channel_store::*;
pub use rpc::*; pub use rpc::*;
pub use telemetry::ClickhouseEvent; pub use telemetry::ClickhouseEvent;
pub use user::*; pub use user::*;
@ -535,6 +540,7 @@ impl Client {
} }
} }
#[track_caller]
pub fn add_message_handler<M, E, H, F>( pub fn add_message_handler<M, E, H, F>(
self: &Arc<Self>, self: &Arc<Self>,
model: ModelHandle<E>, model: ModelHandle<E>,
@ -570,7 +576,13 @@ impl Client {
}), }),
); );
if prev_handler.is_some() { if prev_handler.is_some() {
panic!("registered handler for the same message twice"); let location = std::panic::Location::caller();
panic!(
"{}:{} registered handler for the same message {} twice",
location.file(),
location.line(),
std::any::type_name::<M>()
);
} }
Subscription::Message { Subscription::Message {

View file

@ -165,17 +165,29 @@ impl UserStore {
}); });
current_user_tx.send(user).await.ok(); current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| {
cx.notify();
});
} }
} }
Status::SignedOut => { Status::SignedOut => {
current_user_tx.send(None).await.ok(); current_user_tx.send(None).await.ok();
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, _| this.clear_contacts()).await; this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})
.await;
} }
} }
Status::ConnectionLost => { Status::ConnectionLost => {
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, _| this.clear_contacts()).await; this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})
.await;
} }
} }
_ => {} _ => {}

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.16.0" version = "0.17.0"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -36,7 +36,8 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" ( CREATE TABLE "rooms" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"live_kit_room" VARCHAR NOT NULL "live_kit_room" VARCHAR NOT NULL,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
); );
CREATE TABLE "projects" ( CREATE TABLE "projects" (
@ -184,3 +185,26 @@ CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
CREATE TABLE "channels" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now
);
CREATE TABLE "channel_paths" (
"id_path" TEXT NOT NULL PRIMARY KEY,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
);
CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
CREATE TABLE "channel_members" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"admin" BOOLEAN NOT NULL DEFAULT false,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP NOT NULL DEFAULT now
);
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");

View file

@ -0,0 +1,30 @@
DROP TABLE "channel_messages";
DROP TABLE "channel_memberships";
DROP TABLE "org_memberships";
DROP TABLE "orgs";
DROP TABLE "channels";
CREATE TABLE "channels" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE "channel_paths" (
"id_path" VARCHAR NOT NULL PRIMARY KEY,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
);
CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
CREATE TABLE "channel_members" (
"id" SERIAL PRIMARY KEY,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"admin" BOOLEAN NOT NULL DEFAULT false,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE;

View file

@ -64,9 +64,9 @@ async fn main() {
.expect("failed to fetch user") .expect("failed to fetch user")
.is_none() .is_none()
{ {
if let Some(email) = &github_user.email { if admin {
db.create_user( db.create_user(
email, &format!("{}@zed.dev", github_user.login),
admin, admin,
db::NewUserParams { db::NewUserParams {
github_login: github_user.login, github_login: github_user.login,
@ -76,15 +76,11 @@ async fn main() {
) )
.await .await
.expect("failed to insert user"); .expect("failed to insert user");
} else if admin { } else {
db.create_user( db.get_or_create_user_by_github_account(
&format!("{}@zed.dev", github_user.login), &github_user.login,
admin, Some(github_user.id),
db::NewUserParams { github_user.email.as_deref(),
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
) )
.await .await
.expect("failed to insert user"); .expect("failed to insert user");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
use super::ChannelId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channels")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelId,
pub name: String,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::room::Entity")]
Room,
#[sea_orm(has_many = "super::channel_member::Entity")]
Member,
}
impl Related<super::channel_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::Member.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
// impl Related<super::follower::Entity> for Entity {
// fn to() -> RelationDef {
// Relation::Follower.def()
// }
// }

View file

@ -0,0 +1,61 @@
use crate::db::channel_member;
use super::{ChannelId, ChannelMemberId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_members")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelMemberId,
pub channel_id: ChannelId,
pub user_id: UserId,
pub accepted: bool,
pub admin: bool,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
#[derive(Debug)]
pub struct UserToChannel;
impl Linked for UserToChannel {
type FromEntity = super::user::Entity;
type ToEntity = super::channel::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
channel_member::Relation::User.def().rev(),
channel_member::Relation::Channel.def(),
]
}
}

View file

@ -0,0 +1,15 @@
use super::ChannelId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_paths")]
pub struct Model {
#[sea_orm(primary_key)]
pub id_path: String,
pub channel_id: ChannelId,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

View file

@ -1,12 +1,13 @@
use super::RoomId; use super::{ChannelId, RoomId};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "rooms")] #[sea_orm(table_name = "rooms")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: RoomId, pub id: RoomId,
pub live_kit_room: String, pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -17,6 +18,12 @@ pub enum Relation {
Project, Project,
#[sea_orm(has_many = "super::follower::Entity")] #[sea_orm(has_many = "super::follower::Entity")]
Follower, Follower,
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
} }
impl Related<super::room_participant::Entity> for Entity { impl Related<super::room_participant::Entity> for Entity {
@ -37,4 +44,10 @@ impl Related<super::follower::Entity> for Entity {
} }
} }
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -879,6 +879,453 @@ async fn test_invite_codes() {
assert!(db.has_contact(user5, user1).await.unwrap()); assert!(db.has_contact(user5, user1).await.unwrap());
} }
test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
let a_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let b_id = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
// Make sure that people cannot read channels they haven't been invited to
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
db.invite_channel_member(zed_id, b_id, a_id, false)
.await
.unwrap();
db.respond_to_channel_invite(zed_id, b_id, true)
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.await
.unwrap();
let mut members = db.get_channel_members(replace_id).await.unwrap();
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.await
.unwrap();
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: rust_id,
name: "rust".to_string(),
parent_id: None,
},
Channel {
id: cargo_id,
name: "cargo".to_string(),
parent_id: Some(rust_id),
},
Channel {
id: cargo_ra_id,
name: "cargo-ra".to_string(),
parent_id: Some(cargo_id),
}
]
);
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// Update member permissions
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
assert!(set_subchannel_admin.is_err());
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
assert!(set_channel_admin.is_ok());
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// Remove a single channel
db.remove_channel(crdb_id, a_id).await.unwrap();
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
// Remove a channel tree
let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
channel_ids.sort();
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
assert_eq!(user_ids, &[a_id]);
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
});
test_both_dbs!(
test_joining_channels_postgres,
test_joining_channels_sqlite,
db,
{
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user_1 = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_2 = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
// can join a room with membership to its channel
let joined_room = db
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
drop(joined_room);
// cannot join a room without membership to its channel
assert!(db
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
.await
.is_err());
}
);
test_both_dbs!(
test_channel_invites_postgres,
test_channel_invites_sqlite,
db,
{
db.create_server("test").await.unwrap();
let user_1 = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_2 = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_3 = db
.create_user(
"user3@example.com",
false,
NewUserParams {
github_login: "user3".into(),
github_user_id: 7,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_2, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_3, user_1, true)
.await
.unwrap();
let user_2_invites = db
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
let user_3_invites = db
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_3_invites, &[channel_1_1]);
let members = db
.get_channel_member_details(channel_1_1, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: false,
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: true,
},
]
);
db.respond_to_channel_invite(channel_1_1, user_2, true)
.await
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.await
.unwrap();
let members = db
.get_channel_member_details(channel_1_3, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
admin: false,
},
]
);
}
);
test_both_dbs!(
test_channel_renames_postgres,
test_channel_renames_sqlite,
db,
{
db.create_server("test").await.unwrap();
let user_1 = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_2 = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
.unwrap();
let zed_archive_id = zed_id;
let (channel, _) = db
.get_channel(zed_archive_id, user_1)
.await
.unwrap()
.unwrap();
assert_eq!(channel.name, "zed-archive");
let non_permissioned_rename = db
.rename_channel(zed_archive_id, user_2, "hacked-lol")
.await;
assert!(non_permissioned_rename.is_err());
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
assert!(bad_name_rename.is_err())
}
);
#[gpui::test] #[gpui::test]
async fn test_multiple_signup_overwrite() { async fn test_multiple_signup_overwrite() {
let test_db = TestDb::postgres(build_background_executor()); let test_db = TestDb::postgres(build_background_executor());

View file

@ -26,6 +26,8 @@ pub enum Relation {
RoomParticipant, RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")] #[sea_orm(has_many = "super::project::Entity")]
HostedProjects, HostedProjects,
#[sea_orm(has_many = "super::channel_member::Entity")]
ChannelMemberships,
} }
impl Related<super::access_token::Entity> for Entity { impl Related<super::access_token::Entity> for Entity {
@ -46,4 +48,10 @@ impl Related<super::project::Entity> for Entity {
} }
} }
impl Related<super::channel_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::ChannelMemberships.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -2,7 +2,7 @@ mod connection_pool;
use crate::{ use crate::{
auth, auth,
db::{self, Database, ProjectId, RoomId, ServerId, User, UserId}, db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
executor::Executor, executor::Executor,
AppState, Result, AppState, Result,
}; };
@ -34,7 +34,10 @@ use futures::{
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge}; use prometheus::{register_int_gauge, IntGauge};
use rpc::{ use rpc::{
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, proto::{
self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope, Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
}; };
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
@ -239,6 +242,15 @@ impl Server {
.add_request_handler(request_contact) .add_request_handler(request_contact)
.add_request_handler(remove_contact) .add_request_handler(remove_contact)
.add_request_handler(respond_to_contact_request) .add_request_handler(respond_to_contact_request)
.add_request_handler(create_channel)
.add_request_handler(remove_channel)
.add_request_handler(invite_channel_member)
.add_request_handler(remove_channel_member)
.add_request_handler(set_channel_member_admin)
.add_request_handler(rename_channel)
.add_request_handler(get_channel_members)
.add_request_handler(respond_to_channel_invite)
.add_request_handler(join_channel)
.add_request_handler(follow) .add_request_handler(follow)
.add_message_handler(unfollow) .add_message_handler(unfollow)
.add_message_handler(update_followers) .add_message_handler(update_followers)
@ -287,6 +299,15 @@ impl Server {
"refreshed room" "refreshed room"
); );
room_updated(&refreshed_room.room, &peer); room_updated(&refreshed_room.room, &peer);
if let Some(channel_id) = refreshed_room.channel_id {
channel_updated(
channel_id,
&refreshed_room.room,
&refreshed_room.channel_members,
&peer,
&*pool.lock(),
);
}
contacts_to_update contacts_to_update
.extend(refreshed_room.stale_participant_user_ids.iter().copied()); .extend(refreshed_room.stale_participant_user_ids.iter().copied());
contacts_to_update contacts_to_update
@ -508,15 +529,21 @@ impl Server {
this.app_state.db.set_user_connected_once(user_id, true).await?; this.app_state.db.set_user_connected_once(user_id, true).await?;
} }
let (contacts, invite_code) = future::try_join( let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4(
this.app_state.db.get_contacts(user_id), this.app_state.db.get_contacts(user_id),
this.app_state.db.get_invite_code_for_user(user_id) this.app_state.db.get_invite_code_for_user(user_id),
this.app_state.db.get_channels_for_user(user_id),
this.app_state.db.get_channel_invites_for_user(user_id)
).await?; ).await?;
{ {
let mut pool = this.connection_pool.lock(); let mut pool = this.connection_pool.lock();
pool.add_connection(connection_id, user_id, user.admin); pool.add_connection(connection_id, user_id, user.admin);
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
this.peer.send(connection_id, build_initial_channels_update(
channels_for_user,
channel_invites
))?;
if let Some((code, count)) = invite_code { if let Some((code, count)) = invite_code {
this.peer.send(connection_id, proto::UpdateInviteInfo { this.peer.send(connection_id, proto::UpdateInviteInfo {
@ -857,42 +884,41 @@ async fn create_room(
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let live_kit_room = nanoid::nanoid!(30); let live_kit_room = nanoid::nanoid!(30);
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(_) = live_kit let live_kit_connection_info = {
.create_room(live_kit_room.clone()) let live_kit_room = live_kit_room.clone();
.await let live_kit = session.live_kit_client.as_ref();
.trace_err()
{ util::async_iife!({
if let Some(token) = live_kit let live_kit = live_kit?;
live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &session.user_id.to_string()) .room_token(&live_kit_room, &session.user_id.to_string())
.trace_err() .trace_err()?;
{
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
} else {
None
}
} else {
None
}
} else {
None
};
{ Some(proto::LiveKitConnectionInfo {
let room = session server_url: live_kit.url().into(),
.db() token,
.await })
.create_room(session.user_id, session.connection_id, &live_kit_room) })
.await?;
response.send(proto::CreateRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
} }
.await;
let room = session
.db()
.await
.create_room(session.user_id, session.connection_id, &live_kit_room)
.await?;
response.send(proto::CreateRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
update_user_contacts(session.user_id, &session).await?; update_user_contacts(session.user_id, &session).await?;
Ok(()) Ok(())
@ -904,16 +930,26 @@ async fn join_room(
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let room_id = RoomId::from_proto(request.id); let room_id = RoomId::from_proto(request.id);
let room = { let joined_room = {
let room = session let room = session
.db() .db()
.await .await
.join_room(room_id, session.user_id, session.connection_id) .join_room(room_id, session.user_id, session.connection_id)
.await?; .await?;
room_updated(&room, &session.peer); room_updated(&room.room, &session.peer);
room.clone() room.into_inner()
}; };
if let Some(channel_id) = joined_room.channel_id {
channel_updated(
channel_id,
&joined_room.room,
&joined_room.channel_members,
&session.peer,
&*session.connection_pool().await,
)
}
for connection_id in session for connection_id in session
.connection_pool() .connection_pool()
.await .await
@ -932,7 +968,10 @@ async fn join_room(
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() { let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(token) = live_kit if let Some(token) = live_kit
.room_token(&room.live_kit_room, &session.user_id.to_string()) .room_token(
&joined_room.room.live_kit_room,
&session.user_id.to_string(),
)
.trace_err() .trace_err()
{ {
Some(proto::LiveKitConnectionInfo { Some(proto::LiveKitConnectionInfo {
@ -947,7 +986,8 @@ async fn join_room(
}; };
response.send(proto::JoinRoomResponse { response.send(proto::JoinRoomResponse {
room: Some(room), room: Some(joined_room.room),
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
live_kit_connection_info, live_kit_connection_info,
})?; })?;
@ -960,6 +1000,9 @@ async fn rejoin_room(
response: Response<proto::RejoinRoom>, response: Response<proto::RejoinRoom>,
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let room;
let channel_id;
let channel_members;
{ {
let mut rejoined_room = session let mut rejoined_room = session
.db() .db()
@ -1121,6 +1164,22 @@ async fn rejoin_room(
)?; )?;
} }
} }
let rejoined_room = rejoined_room.into_inner();
room = rejoined_room.room;
channel_id = rejoined_room.channel_id;
channel_members = rejoined_room.channel_members;
}
if let Some(channel_id) = channel_id {
channel_updated(
channel_id,
&room,
&channel_members,
&session.peer,
&*session.connection_pool().await,
);
} }
update_user_contacts(session.user_id, &session).await?; update_user_contacts(session.user_id, &session).await?;
@ -1282,11 +1341,12 @@ async fn update_participant_location(
let location = request let location = request
.location .location
.ok_or_else(|| anyhow!("invalid location"))?; .ok_or_else(|| anyhow!("invalid location"))?;
let room = session
.db() let db = session.db().await;
.await let room = db
.update_room_participant_location(room_id, session.connection_id, location) .update_room_participant_location(room_id, session.connection_id, location)
.await?; .await?;
room_updated(&room, &session.peer); room_updated(&room, &session.peer);
response.send(proto::Ack {})?; response.send(proto::Ack {})?;
Ok(()) Ok(())
@ -2084,6 +2144,340 @@ async fn remove_contact(
Ok(()) Ok(())
} }
async fn create_channel(
request: proto::CreateChannel,
response: Response<proto::CreateChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit.create_room(live_kit_room.clone()).await?;
}
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let id = db
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
.await?;
let channel = proto::Channel {
id: id.to_proto(),
name: request.name,
parent_id: request.parent_id,
};
response.send(proto::ChannelResponse {
channel: Some(channel.clone()),
})?;
let mut update = proto::UpdateChannels::default();
update.channels.push(channel);
let user_ids_to_notify = if let Some(parent_id) = parent_id {
db.get_channel_members(parent_id).await?
} else {
vec![session.user_id]
};
let connection_pool = session.connection_pool().await;
for user_id in user_ids_to_notify {
for connection_id in connection_pool.user_connection_ids(user_id) {
let mut update = update.clone();
if user_id == session.user_id {
update.channel_permissions.push(proto::ChannelPermission {
channel_id: id.to_proto(),
is_admin: true,
});
}
session.peer.send(connection_id, update)?;
}
}
Ok(())
}
async fn remove_channel(
request: proto::RemoveChannel,
response: Response<proto::RemoveChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = request.channel_id;
let (removed_channels, member_ids) = db
.remove_channel(ChannelId::from_proto(channel_id), session.user_id)
.await?;
response.send(proto::Ack {})?;
// Notify members of removed channels
let mut update = proto::UpdateChannels::default();
update
.remove_channels
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
let connection_pool = session.connection_pool().await;
for member_id in member_ids {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
Ok(())
}
async fn invite_channel_member(
request: proto::InviteChannelMember,
response: Response<proto::InviteChannelMember>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let invitee_id = UserId::from_proto(request.user_id);
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
.await?;
let (channel, _) = db
.get_channel(channel_id, session.user_id)
.await?
.ok_or_else(|| anyhow!("channel not found"))?;
let mut update = proto::UpdateChannels::default();
update.channel_invitations.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: None,
});
for connection_id in session
.connection_pool()
.await
.user_connection_ids(invitee_id)
{
session.peer.send(connection_id, update.clone())?;
}
response.send(proto::Ack {})?;
Ok(())
}
async fn remove_channel_member(
request: proto::RemoveChannelMember,
response: Response<proto::RemoveChannelMember>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let member_id = UserId::from_proto(request.user_id);
db.remove_channel_member(channel_id, member_id, session.user_id)
.await?;
let mut update = proto::UpdateChannels::default();
update.remove_channels.push(channel_id.to_proto());
for connection_id in session
.connection_pool()
.await
.user_connection_ids(member_id)
{
session.peer.send(connection_id, update.clone())?;
}
response.send(proto::Ack {})?;
Ok(())
}
async fn set_channel_member_admin(
request: proto::SetChannelMemberAdmin,
response: Response<proto::SetChannelMemberAdmin>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let member_id = UserId::from_proto(request.user_id);
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
.await?;
let (channel, has_accepted) = db
.get_channel(channel_id, member_id)
.await?
.ok_or_else(|| anyhow!("channel not found"))?;
let mut update = proto::UpdateChannels::default();
if has_accepted {
update.channel_permissions.push(proto::ChannelPermission {
channel_id: channel.id.to_proto(),
is_admin: request.admin,
});
}
for connection_id in session
.connection_pool()
.await
.user_connection_ids(member_id)
{
session.peer.send(connection_id, update.clone())?;
}
response.send(proto::Ack {})?;
Ok(())
}
async fn rename_channel(
request: proto::RenameChannel,
response: Response<proto::RenameChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let new_name = db
.rename_channel(channel_id, session.user_id, &request.name)
.await?;
let channel = proto::Channel {
id: request.channel_id,
name: new_name,
parent_id: None,
};
response.send(proto::ChannelResponse {
channel: Some(channel.clone()),
})?;
let mut update = proto::UpdateChannels::default();
update.channels.push(channel);
let member_ids = db.get_channel_members(channel_id).await?;
let connection_pool = session.connection_pool().await;
for member_id in member_ids {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
Ok(())
}
async fn get_channel_members(
request: proto::GetChannelMembers,
response: Response<proto::GetChannelMembers>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let members = db
.get_channel_member_details(channel_id, session.user_id)
.await?;
response.send(proto::GetChannelMembersResponse { members })?;
Ok(())
}
async fn respond_to_channel_invite(
request: proto::RespondToChannelInvite,
response: Response<proto::RespondToChannelInvite>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
.await?;
let mut update = proto::UpdateChannels::default();
update
.remove_channel_invitations
.push(channel_id.to_proto());
if request.accept {
let result = db.get_channels_for_user(session.user_id).await?;
update
.channels
.extend(result.channels.into_iter().map(|channel| proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: channel.parent_id.map(ChannelId::to_proto),
}));
update
.channel_participants
.extend(
result
.channel_participants
.into_iter()
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
}),
);
update
.channel_permissions
.extend(
result
.channels_with_admin_privileges
.into_iter()
.map(|channel_id| proto::ChannelPermission {
channel_id: channel_id.to_proto(),
is_admin: true,
}),
);
}
session.peer.send(session.connection_id, update)?;
response.send(proto::Ack {})?;
Ok(())
}
async fn join_channel(
request: proto::JoinChannel,
response: Response<proto::JoinChannel>,
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
let joined_room = {
leave_room_for_session(&session).await?;
let db = session.db().await;
let room_id = db.room_id_for_channel(channel_id).await?;
let joined_room = db
.join_room(room_id, session.user_id, session.connection_id)
.await?;
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
let token = live_kit
.room_token(
&joined_room.room.live_kit_room,
&session.user_id.to_string(),
)
.trace_err()?;
Some(LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
});
response.send(proto::JoinRoomResponse {
room: Some(joined_room.room.clone()),
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
live_kit_connection_info,
})?;
room_updated(&joined_room.room, &session.peer);
joined_room.into_inner()
};
channel_updated(
channel_id,
&joined_room.room,
&joined_room.channel_members,
&session.peer,
&*session.connection_pool().await,
);
update_user_contacts(session.user_id, &session).await?;
Ok(())
}
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session let project_connection_ids = session
@ -2154,6 +2548,52 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {
} }
} }
fn build_initial_channels_update(
channels: ChannelsForUser,
channel_invites: Vec<db::Channel>,
) -> proto::UpdateChannels {
let mut update = proto::UpdateChannels::default();
for channel in channels.channels {
update.channels.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: channel.parent_id.map(|id| id.to_proto()),
});
}
for (channel_id, participants) in channels.channel_participants {
update
.channel_participants
.push(proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: participants.into_iter().map(|id| id.to_proto()).collect(),
});
}
update
.channel_permissions
.extend(
channels
.channels_with_admin_privileges
.into_iter()
.map(|id| proto::ChannelPermission {
channel_id: id.to_proto(),
is_admin: true,
}),
);
for channel in channel_invites {
update.channel_invitations.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: None,
});
}
update
}
fn build_initial_contacts_update( fn build_initial_contacts_update(
contacts: Vec<db::Contact>, contacts: Vec<db::Contact>,
pool: &ConnectionPool, pool: &ConnectionPool,
@ -2218,8 +2658,42 @@ fn room_updated(room: &proto::Room, peer: &Peer) {
); );
} }
fn channel_updated(
channel_id: ChannelId,
room: &proto::Room,
channel_members: &[UserId],
peer: &Peer,
pool: &ConnectionPool,
) {
let participants = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
broadcast(
None,
channel_members
.iter()
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|peer_id| {
peer.send(
peer_id.into(),
proto::UpdateChannels {
channel_participants: vec![proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: participants.clone(),
}],
..Default::default()
},
)
},
);
}
async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> { async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
let db = session.db().await; let db = session.db().await;
let contacts = db.get_contacts(user_id).await?; let contacts = db.get_contacts(user_id).await?;
let busy = db.is_user_busy(user_id).await?; let busy = db.is_user_busy(user_id).await?;
@ -2259,6 +2733,10 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
let canceled_calls_to_user_ids; let canceled_calls_to_user_ids;
let live_kit_room; let live_kit_room;
let delete_live_kit_room; let delete_live_kit_room;
let room;
let channel_members;
let channel_id;
if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? { if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? {
contacts_to_update.insert(session.user_id); contacts_to_update.insert(session.user_id);
@ -2266,15 +2744,29 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
project_left(project, session); project_left(project, session);
} }
room_updated(&left_room.room, &session.peer);
room_id = RoomId::from_proto(left_room.room.id); room_id = RoomId::from_proto(left_room.room.id);
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut left_room.room.live_kit_room); live_kit_room = mem::take(&mut left_room.room.live_kit_room);
delete_live_kit_room = left_room.room.participants.is_empty(); delete_live_kit_room = left_room.deleted;
room = mem::take(&mut left_room.room);
channel_members = mem::take(&mut left_room.channel_members);
channel_id = left_room.channel_id;
room_updated(&room, &session.peer);
} else { } else {
return Ok(()); return Ok(());
} }
if let Some(channel_id) = channel_id {
channel_updated(
channel_id,
&room,
&channel_members,
&session.peer,
&*session.connection_pool().await,
);
}
{ {
let pool = session.connection_pool().await; let pool = session.connection_pool().await;
for canceled_user_id in canceled_calls_to_user_ids { for canceled_user_id in canceled_calls_to_user_ids {

View file

@ -5,14 +5,15 @@ use crate::{
AppState, AppState,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use call::ActiveCall; use call::{ActiveCall, Room};
use client::{ use client::{
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
UserStore,
}; };
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::FakeFs; use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _}; use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
use language::LanguageRegistry; use language::LanguageRegistry;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Project, WorktreeId}; use project::{Project, WorktreeId};
@ -30,6 +31,7 @@ use std::{
use util::http::FakeHttpClient; use util::http::FakeHttpClient;
use workspace::Workspace; use workspace::Workspace;
mod channel_tests;
mod integration_tests; mod integration_tests;
mod randomized_integration_tests; mod randomized_integration_tests;
@ -98,6 +100,9 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| { cx.update(|cx| {
if cx.has_global::<SettingsStore>() {
panic!("Same cx used to create two test clients")
}
cx.set_global(SettingsStore::test(cx)); cx.set_global(SettingsStore::test(cx));
}); });
@ -183,13 +188,16 @@ impl TestServer {
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let app_state = Arc::new(workspace::AppState { let app_state = Arc::new(workspace::AppState {
client: client.clone(), client: client.clone(),
user_store: user_store.clone(), user_store: user_store.clone(),
channel_store: channel_store.clone(),
languages: Arc::new(LanguageRegistry::test()), languages: Arc::new(LanguageRegistry::test()),
fs: fs.clone(), fs: fs.clone(),
build_window_options: |_, _, _| Default::default(), build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| unimplemented!(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[], background_actions: || &[],
}); });
@ -210,12 +218,9 @@ impl TestServer {
.unwrap(); .unwrap();
let client = TestClient { let client = TestClient {
client, app_state,
username: name.to_string(), username: name.to_string(),
state: Default::default(), state: Default::default(),
user_store,
fs,
language_registry: Arc::new(LanguageRegistry::test()),
}; };
client.wait_for_current_user(cx).await; client.wait_for_current_user(cx).await;
client client
@ -243,6 +248,7 @@ impl TestServer {
let (client_a, cx_a) = left.last_mut().unwrap(); let (client_a, cx_a) = left.last_mut().unwrap();
for (client_b, cx_b) in right { for (client_b, cx_b) in right {
client_a client_a
.app_state
.user_store .user_store
.update(*cx_a, |store, cx| { .update(*cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap(), cx) store.request_contact(client_b.user_id().unwrap(), cx)
@ -251,6 +257,7 @@ impl TestServer {
.unwrap(); .unwrap();
cx_a.foreground().run_until_parked(); cx_a.foreground().run_until_parked();
client_b client_b
.app_state
.user_store .user_store
.update(*cx_b, |store, cx| { .update(*cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
@ -261,6 +268,52 @@ impl TestServer {
} }
} }
async fn make_channel(
&self,
channel: &str,
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
let (admin_client, admin_cx) = admin;
let channel_id = admin_client
.app_state
.channel_store
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, None, cx)
})
.await
.unwrap();
for (member_client, member_cx) in members {
admin_client
.app_state
.channel_store
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
member_client.user_id().unwrap(),
false,
cx,
)
})
.await
.unwrap();
admin_cx.foreground().run_until_parked();
member_client
.app_state
.channel_store
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
.await
.unwrap();
}
channel_id
}
async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
self.make_contacts(clients).await; self.make_contacts(clients).await;
@ -312,12 +365,9 @@ impl Drop for TestServer {
} }
struct TestClient { struct TestClient {
client: Arc<Client>,
username: String, username: String,
state: RefCell<TestClientState>, state: RefCell<TestClientState>,
pub user_store: ModelHandle<UserStore>, app_state: Arc<workspace::AppState>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<FakeFs>,
} }
#[derive(Default)] #[derive(Default)]
@ -331,7 +381,7 @@ impl Deref for TestClient {
type Target = Arc<Client>; type Target = Arc<Client>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.client &self.app_state.client
} }
} }
@ -342,22 +392,45 @@ struct ContactsSummary {
} }
impl TestClient { impl TestClient {
pub fn fs(&self) -> &FakeFs {
self.app_state.fs.as_fake()
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
&self.app_state.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.app_state.user_store
}
pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
&self.app_state.languages
}
pub fn client(&self) -> &Arc<Client> {
&self.app_state.client
}
pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
UserId::from_proto( UserId::from_proto(
self.user_store self.app_state
.user_store
.read_with(cx, |user_store, _| user_store.current_user().unwrap().id), .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
) )
} }
async fn wait_for_current_user(&self, cx: &TestAppContext) { async fn wait_for_current_user(&self, cx: &TestAppContext) {
let mut authed_user = self let mut authed_user = self
.app_state
.user_store .user_store
.read_with(cx, |user_store, _| user_store.watch_current_user()); .read_with(cx, |user_store, _| user_store.watch_current_user());
while authed_user.next().await.unwrap().is_none() {} while authed_user.next().await.unwrap().is_none() {}
} }
async fn clear_contacts(&self, cx: &mut TestAppContext) { async fn clear_contacts(&self, cx: &mut TestAppContext) {
self.user_store self.app_state
.user_store
.update(cx, |store, _| store.clear_contacts()) .update(cx, |store, _| store.clear_contacts())
.await; .await;
} }
@ -395,23 +468,25 @@ impl TestClient {
} }
fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
self.user_store.read_with(cx, |store, _| ContactsSummary { self.app_state
current: store .user_store
.contacts() .read_with(cx, |store, _| ContactsSummary {
.iter() current: store
.map(|contact| contact.user.github_login.clone()) .contacts()
.collect(), .iter()
outgoing_requests: store .map(|contact| contact.user.github_login.clone())
.outgoing_contact_requests() .collect(),
.iter() outgoing_requests: store
.map(|user| user.github_login.clone()) .outgoing_contact_requests()
.collect(), .iter()
incoming_requests: store .map(|user| user.github_login.clone())
.incoming_contact_requests() .collect(),
.iter() incoming_requests: store
.map(|user| user.github_login.clone()) .incoming_contact_requests()
.collect(), .iter()
}) .map(|user| user.github_login.clone())
.collect(),
})
} }
async fn build_local_project( async fn build_local_project(
@ -421,10 +496,10 @@ impl TestClient {
) -> (ModelHandle<Project>, WorktreeId) { ) -> (ModelHandle<Project>, WorktreeId) {
let project = cx.update(|cx| { let project = cx.update(|cx| {
Project::local( Project::local(
self.client.clone(), self.client().clone(),
self.user_store.clone(), self.app_state.user_store.clone(),
self.language_registry.clone(), self.app_state.languages.clone(),
self.fs.clone(), self.app_state.fs.clone(),
cx, cx,
) )
}); });
@ -450,8 +525,8 @@ impl TestClient {
room.update(guest_cx, |room, cx| { room.update(guest_cx, |room, cx| {
room.join_project( room.join_project(
host_project_id, host_project_id,
self.language_registry.clone(), self.app_state.languages.clone(),
self.fs.clone(), self.app_state.fs.clone(),
cx, cx,
) )
}) })
@ -464,12 +539,36 @@ impl TestClient {
project: &ModelHandle<Project>, project: &ModelHandle<Project>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> WindowHandle<Workspace> { ) -> WindowHandle<Workspace> {
cx.add_window(|cx| Workspace::test_new(project.clone(), cx)) cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
} }
} }
impl Drop for TestClient { impl Drop for TestClient {
fn drop(&mut self) { fn drop(&mut self) {
self.client.teardown(); self.app_state.client.teardown();
} }
} }
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}

View file

@ -0,0 +1,922 @@
use crate::{
rpc::RECONNECT_TIMEOUT,
tests::{room_participants, RoomParticipants, TestServer},
};
use call::ActiveCall;
use client::{ChannelId, ChannelMembership, ChannelStore, User};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use rpc::{proto, RECEIVE_TIMEOUT};
use std::sync::Arc;
#[gpui::test]
async fn test_core_channels(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_a_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-a", None, cx)
})
.await
.unwrap();
let channel_b_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-b", Some(channel_a_id), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
depth: 1,
user_is_admin: true,
},
],
);
client_b.channel_store().read_with(cx_b, |channels, _| {
assert!(channels.channels().collect::<Vec<_>>().is_empty())
});
// Invite client B to channel A as client A.
client_a
.channel_store()
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
// Make sure we're synchronously storing the pending invite
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
invite
})
.await
.unwrap();
// Client A sees that B has been invited.
deterministic.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: false,
}],
);
let members = client_a
.channel_store()
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
store.get_channel_member_details(channel_a_id, cx)
})
.await
.unwrap();
assert_members_eq(
&members,
&[
(
client_a.user_id().unwrap(),
true,
proto::channel_member::Kind::Member,
),
(
client_b.user_id().unwrap(),
false,
proto::channel_member::Kind::Invitee,
),
],
);
// Client B accepts the invitation.
client_b
.channel_store()
.update(cx_b, |channels, _| {
channels.respond_to_channel_invite(channel_a_id, true)
})
.await
.unwrap();
deterministic.run_until_parked();
// Client B now sees that they are a member of channel A and its existing subchannels.
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
user_is_admin: false,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
user_is_admin: false,
depth: 1,
},
],
);
let channel_c_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-c", Some(channel_b_id), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
user_is_admin: false,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
user_is_admin: false,
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
user_is_admin: false,
depth: 2,
},
],
);
// Update client B's membership to channel A to be an admin.
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Observe that client B is now an admin of channel A, and that
// their admin priveleges extend to subchannels of channel A.
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
depth: 1,
user_is_admin: true,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
depth: 2,
user_is_admin: true,
},
],
);
// Client A deletes the channel, deletion also deletes subchannels.
client_a
.channel_store()
.update(cx_a, |channel_store, _| {
channel_store.remove_channel(channel_b_id)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
}],
);
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
}],
);
// Remove client B
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Client A still has their channel
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
}],
);
// Client B no longer has access to the channel
assert_channels(client_b.channel_store(), cx_b, &[]);
// When disconnected, client A sees no channels.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
assert_channels(client_a.channel_store(), cx_a, &[]);
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
}],
);
}
#[track_caller]
fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u64]) {
assert_eq!(
participants.iter().map(|p| p.id).collect::<Vec<_>>(),
expected_partitipants
);
}
#[track_caller]
fn assert_members_eq(
members: &[ChannelMembership],
expected_members: &[(u64, bool, proto::channel_member::Kind)],
) {
assert_eq!(
members
.iter()
.map(|member| (member.user.id, member.admin, member.kind))
.collect::<Vec<_>>(),
expected_members
);
}
#[gpui::test]
async fn test_joining_channel_ancestor_member(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let parent_id = server
.make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
let sub_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("sub_channel", Some(parent_id), cx)
})
.await
.unwrap();
let active_call_b = cx_b.read(ActiveCall::global);
assert!(active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
.await
.is_ok());
}
#[gpui::test]
async fn test_channel_room(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let zed_id = server
.make_channel(
"zed",
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
// Give everyone a chance to observe user A joining
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
});
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: zed_id,
name: "zed".to_string(),
depth: 0,
user_is_admin: false,
}],
);
client_b.channel_store().read_with(cx_b, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
});
client_c.channel_store().read_with(cx_c, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
});
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
client_b.channel_store().read_with(cx_b, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
client_c.channel_store().read_with(cx_c, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec![]
}
);
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec![]
}
);
// Make sure that leaving and rejoining works
active_call_a
.update(cx_a, |active_call, cx| active_call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
});
client_b.channel_store().read_with(cx_b, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
});
client_c.channel_store().read_with(cx_c, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
});
active_call_b
.update(cx_b, |active_call, cx| active_call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
});
client_b.channel_store().read_with(cx_b, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
});
client_c.channel_store().read_with(cx_c, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
});
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec![]
}
);
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec![]
}
);
}
#[gpui::test]
async fn test_channel_jumping(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
let rust_id = server
.make_channel("rust", (&client_a, cx_a), &mut [])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
// Give everything a chance to observe user A joining
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
assert_participants_eq(channels.channel_participants(rust_id), &[]);
});
active_call_a
.update(cx_a, |active_call, cx| {
active_call.join_channel(rust_id, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
assert_participants_eq(
channels.channel_participants(rust_id),
&[client_a.user_id().unwrap()],
);
});
}
#[gpui::test]
async fn test_permissions_update_while_invited(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let rust_id = server
.make_channel("rust", (&client_a, cx_a), &mut [])
.await;
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".to_string(),
user_is_admin: false,
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
// Update B's invite before they've accepted it
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".to_string(),
user_is_admin: false,
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
}
#[gpui::test]
async fn test_channel_rename(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let rust_id = server
.make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
// Rename the channel
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.rename(rust_id, "#rust-archive", cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Client A sees the channel with its new name.
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".to_string(),
user_is_admin: true,
}],
);
// Client B sees the channel with its new name.
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".to_string(),
user_is_admin: false,
}],
);
}
#[gpui::test]
async fn test_call_from_channel(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let channel_id = server
.make_channel(
"x",
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
// Client A calls client B while in the channel.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
})
.await
.unwrap();
// Client B accepts the call.
deterministic.run_until_parked();
active_call_b
.update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
// Client B sees that they are now in the channel
deterministic.run_until_parked();
active_call_b.read_with(cx_b, |call, cx| {
assert_eq!(call.channel_id(cx), Some(channel_id));
});
client_b.channel_store().read_with(cx_b, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
// Clients A and C also see that client B is in the channel.
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
client_c.channel_store().read_with(cx_c, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
});
}
#[gpui::test]
async fn test_lost_channel_creation(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await;
// Invite a member
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Sanity check
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
user_is_admin: false,
}],
);
let subchannel_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("subchannel", Some(channel_id), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Make sure A sees their new channel
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
user_is_admin: true,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".to_string(),
user_is_admin: true,
},
],
);
// Accept the invite
client_b
.channel_store()
.update(cx_b, |channel_store, _| {
channel_store.respond_to_channel_invite(channel_id, true)
})
.await
.unwrap();
deterministic.run_until_parked();
// B should now see the channel
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
user_is_admin: false,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".to_string(),
user_is_admin: false,
},
],
);
}
#[derive(Debug, PartialEq)]
struct ExpectedChannel {
depth: usize,
id: ChannelId,
name: String,
user_is_admin: bool,
}
#[track_caller]
fn assert_channel_invitations(
channel_store: &ModelHandle<ChannelStore>,
cx: &TestAppContext,
expected_channels: &[ExpectedChannel],
) {
let actual = channel_store.read_with(cx, |store, _| {
store
.channel_invitations()
.iter()
.map(|channel| ExpectedChannel {
depth: 0,
name: channel.name.clone(),
id: channel.id,
user_is_admin: store.is_user_admin(channel.id),
})
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
}
#[track_caller]
fn assert_channels(
channel_store: &ModelHandle<ChannelStore>,
cx: &TestAppContext,
expected_channels: &[ExpectedChannel],
) {
let actual = channel_store.read_with(cx, |store, _| {
store
.channels()
.map(|(depth, channel)| ExpectedChannel {
depth,
name: channel.name.clone(),
id: channel.id,
user_is_admin: store.is_user_admin(channel.id),
})
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
}

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
tests::{TestClient, TestServer}, tests::{room_participants, RoomParticipants, TestClient, TestServer},
}; };
use call::{room, ActiveCall, ParticipantLocation, Room}; use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT}; use client::{User, RECEIVE_TIMEOUT};
@ -748,7 +748,7 @@ async fn test_server_restarts(
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
client_a client_a
.fs .fs()
.insert_tree("/a", json!({ "a.txt": "a-contents" })) .insert_tree("/a", json!({ "a.txt": "a-contents" }))
.await; .await;
@ -1220,7 +1220,7 @@ async fn test_share_project(
let active_call_c = cx_c.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -1387,7 +1387,7 @@ async fn test_unshare_project(
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -1476,7 +1476,7 @@ async fn test_host_disconnect(
cx_b.update(editor::init); cx_b.update(editor::init);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -1498,7 +1498,8 @@ async fn test_host_disconnect(
deterministic.run_until_parked(); deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let window_b =
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
let workspace_b = window_b.root(cx_b); let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
@ -1581,7 +1582,7 @@ async fn test_project_reconnect(
cx_b.update(editor::init); cx_b.update(editor::init);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-1", "/root-1",
json!({ json!({
@ -1609,7 +1610,7 @@ async fn test_project_reconnect(
) )
.await; .await;
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-2", "/root-2",
json!({ json!({
@ -1618,7 +1619,7 @@ async fn test_project_reconnect(
) )
.await; .await;
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-3", "/root-3",
json!({ json!({
@ -1698,7 +1699,7 @@ async fn test_project_reconnect(
// While client A is disconnected, add and remove files from client A's project. // While client A is disconnected, add and remove files from client A's project.
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-1/dir1/subdir2", "/root-1/dir1/subdir2",
json!({ json!({
@ -1710,7 +1711,7 @@ async fn test_project_reconnect(
) )
.await; .await;
client_a client_a
.fs .fs()
.remove_dir( .remove_dir(
"/root-1/dir1/subdir1".as_ref(), "/root-1/dir1/subdir1".as_ref(),
RemoveOptions { RemoveOptions {
@ -1832,11 +1833,11 @@ async fn test_project_reconnect(
// While client B is disconnected, add and remove files from client A's project // While client B is disconnected, add and remove files from client A's project
client_a client_a
.fs .fs()
.insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into()) .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
.await; .await;
client_a client_a
.fs .fs()
.remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default()) .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
@ -1922,8 +1923,8 @@ async fn test_active_call_events(
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
client_a.fs.insert_tree("/a", json!({})).await; client_a.fs().insert_tree("/a", json!({})).await;
client_b.fs.insert_tree("/b", json!({})).await; client_b.fs().insert_tree("/b", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let (project_b, _) = client_b.build_local_project("/b", cx_b).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
@ -2011,8 +2012,8 @@ async fn test_room_location(
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
client_a.fs.insert_tree("/a", json!({})).await; client_a.fs().insert_tree("/a", json!({})).await;
client_b.fs.insert_tree("/b", json!({})).await; client_b.fs().insert_tree("/b", json!({})).await;
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
@ -2201,12 +2202,12 @@ async fn test_propagate_saves_and_fs_changes(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
)); ));
for client in [&client_a, &client_b, &client_c] { for client in [&client_a, &client_b, &client_c] {
client.language_registry.add(rust.clone()); client.language_registry().add(rust.clone());
client.language_registry.add(javascript.clone()); client.language_registry().add(javascript.clone());
} }
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -2276,7 +2277,7 @@ async fn test_propagate_saves_and_fs_changes(
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
save_b.await.unwrap(); save_b.await.unwrap();
assert_eq!( assert_eq!(
client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(), client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
"hi-a, i-am-c, i-am-b, i-am-a" "hi-a, i-am-c, i-am-b, i-am-a"
); );
@ -2287,7 +2288,7 @@ async fn test_propagate_saves_and_fs_changes(
// Make changes on host's file system, see those changes on guest worktrees. // Make changes on host's file system, see those changes on guest worktrees.
client_a client_a
.fs .fs()
.rename( .rename(
"/a/file1.rs".as_ref(), "/a/file1.rs".as_ref(),
"/a/file1.js".as_ref(), "/a/file1.js".as_ref(),
@ -2296,11 +2297,11 @@ async fn test_propagate_saves_and_fs_changes(
.await .await
.unwrap(); .unwrap();
client_a client_a
.fs .fs()
.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
client_a.fs.insert_file("/a/file4", "4".into()).await; client_a.fs().insert_file("/a/file4", "4".into()).await;
deterministic.run_until_parked(); deterministic.run_until_parked();
worktree_a.read_with(cx_a, |tree, _| { worktree_a.read_with(cx_a, |tree, _| {
@ -2394,7 +2395,7 @@ async fn test_git_diff_base_change(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2438,7 +2439,7 @@ async fn test_git_diff_base_change(
" "
.unindent(); .unindent();
client_a.fs.as_fake().set_index_for_repo( client_a.fs().set_index_for_repo(
Path::new("/dir/.git"), Path::new("/dir/.git"),
&[(Path::new("a.txt"), diff_base.clone())], &[(Path::new("a.txt"), diff_base.clone())],
); );
@ -2483,7 +2484,7 @@ async fn test_git_diff_base_change(
); );
}); });
client_a.fs.as_fake().set_index_for_repo( client_a.fs().set_index_for_repo(
Path::new("/dir/.git"), Path::new("/dir/.git"),
&[(Path::new("a.txt"), new_diff_base.clone())], &[(Path::new("a.txt"), new_diff_base.clone())],
); );
@ -2528,7 +2529,7 @@ async fn test_git_diff_base_change(
" "
.unindent(); .unindent();
client_a.fs.as_fake().set_index_for_repo( client_a.fs().set_index_for_repo(
Path::new("/dir/sub/.git"), Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), diff_base.clone())], &[(Path::new("b.txt"), diff_base.clone())],
); );
@ -2573,7 +2574,7 @@ async fn test_git_diff_base_change(
); );
}); });
client_a.fs.as_fake().set_index_for_repo( client_a.fs().set_index_for_repo(
Path::new("/dir/sub/.git"), Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), new_diff_base.clone())], &[(Path::new("b.txt"), new_diff_base.clone())],
); );
@ -2632,7 +2633,7 @@ async fn test_git_branch_name(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2651,8 +2652,7 @@ async fn test_git_branch_name(
let project_remote = client_b.build_remote_project(project_id, cx_b).await; let project_remote = client_b.build_remote_project(project_id, cx_b).await;
client_a client_a
.fs .fs()
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-1")); .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
// Wait for it to catch up to the new branch // Wait for it to catch up to the new branch
@ -2677,8 +2677,7 @@ async fn test_git_branch_name(
}); });
client_a client_a
.fs .fs()
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-2")); .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
// Wait for buffer_local_a to receive it // Wait for buffer_local_a to receive it
@ -2717,7 +2716,7 @@ async fn test_git_status_sync(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2731,7 +2730,7 @@ async fn test_git_status_sync(
const A_TXT: &'static str = "a.txt"; const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt"; const B_TXT: &'static str = "b.txt";
client_a.fs.as_fake().set_status_for_repo_via_git_operation( client_a.fs().set_status_for_repo_via_git_operation(
Path::new("/dir/.git"), Path::new("/dir/.git"),
&[ &[
(&Path::new(A_TXT), GitFileStatus::Added), (&Path::new(A_TXT), GitFileStatus::Added),
@ -2777,16 +2776,13 @@ async fn test_git_status_sync(
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
}); });
client_a client_a.fs().set_status_for_repo_via_working_copy_change(
.fs Path::new("/dir/.git"),
.as_fake() &[
.set_status_for_repo_via_working_copy_change( (&Path::new(A_TXT), GitFileStatus::Modified),
Path::new("/dir/.git"), (&Path::new(B_TXT), GitFileStatus::Modified),
&[ ],
(&Path::new(A_TXT), GitFileStatus::Modified), );
(&Path::new(B_TXT), GitFileStatus::Modified),
],
);
// Wait for buffer_local_a to receive it // Wait for buffer_local_a to receive it
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -2857,7 +2853,7 @@ async fn test_fs_operations(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3130,7 +3126,7 @@ async fn test_local_settings(
// As client A, open a project that contains some local settings files // As client A, open a project that contains some local settings files
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3172,7 +3168,7 @@ async fn test_local_settings(
// As client A, update a settings file. As Client B, see the changed settings. // As client A, update a settings file. As Client B, see the changed settings.
client_a client_a
.fs .fs()
.insert_file("/dir/.zed/settings.json", r#"{}"#.into()) .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
.await; .await;
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -3189,17 +3185,17 @@ async fn test_local_settings(
// As client A, create and remove some settings files. As client B, see the changed settings. // As client A, create and remove some settings files. As client B, see the changed settings.
client_a client_a
.fs .fs()
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
client_a client_a
.fs .fs()
.create_dir("/dir/b/.zed".as_ref()) .create_dir("/dir/b/.zed".as_ref())
.await .await
.unwrap(); .unwrap();
client_a client_a
.fs .fs()
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
.await; .await;
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -3220,11 +3216,11 @@ async fn test_local_settings(
// As client A, change and remove settings files while client B is disconnected. // As client A, change and remove settings files while client B is disconnected.
client_a client_a
.fs .fs()
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
.await; .await;
client_a client_a
.fs .fs()
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
@ -3258,7 +3254,7 @@ async fn test_buffer_conflict_after_save(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3320,7 +3316,7 @@ async fn test_buffer_reloading(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3348,7 +3344,7 @@ async fn test_buffer_reloading(
let new_contents = Rope::from("d\ne\nf"); let new_contents = Rope::from("d\ne\nf");
client_a client_a
.fs .fs()
.save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
.await .await
.unwrap(); .unwrap();
@ -3377,7 +3373,7 @@ async fn test_editing_while_guest_opens_buffer(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree("/dir", json!({ "a.txt": "a-contents" })) .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@ -3426,7 +3422,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree("/dir", json!({ "a.txt": "Some text\n" })) .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@ -3520,7 +3516,7 @@ async fn test_leaving_worktree_while_opening_buffer(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree("/dir", json!({ "a.txt": "a-contents" })) .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@ -3563,7 +3559,7 @@ async fn test_canceling_buffer_opening(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3619,7 +3615,7 @@ async fn test_leaving_project(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -3707,9 +3703,9 @@ async fn test_leaving_project(
cx_b.spawn(|cx| { cx_b.spawn(|cx| {
Project::remote( Project::remote(
project_id, project_id,
client_b.client.clone(), client_b.app_state.client.clone(),
client_b.user_store.clone(), client_b.user_store().clone(),
client_b.language_registry.clone(), client_b.language_registry().clone(),
FakeFs::new(cx.background()), FakeFs::new(cx.background()),
cx, cx,
) )
@ -3761,11 +3757,11 @@ async fn test_collaborating_with_diagnostics(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
// Share a project as client A // Share a project as client A
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -4033,11 +4029,11 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"]; let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/test", "/test",
json!({ json!({
@ -4174,10 +4170,10 @@ async fn test_collaborating_with_completion(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -4335,7 +4331,7 @@ async fn test_reloading_buffer_manually(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree("/a", json!({ "a.rs": "let one = 1;" })) .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
@ -4366,7 +4362,7 @@ async fn test_reloading_buffer_manually(
buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
client_a client_a
.fs .fs()
.save( .save(
"/a/a.rs".as_ref(), "/a/a.rs".as_ref(),
&Rope::from("let seven = 7;"), &Rope::from("let seven = 7;"),
@ -4437,14 +4433,14 @@ async fn test_formatting_buffer(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
// Here we insert a fake tree with a directory that exists on disk. This is needed // Here we insert a fake tree with a directory that exists on disk. This is needed
// because later we'll invoke a command, which requires passing a working directory // because later we'll invoke a command, which requires passing a working directory
// that points to a valid location on disk. // that points to a valid location on disk.
let directory = env::current_dir().unwrap(); let directory = env::current_dir().unwrap();
client_a client_a
.fs .fs()
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
@ -4546,10 +4542,10 @@ async fn test_definition(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root", "/root",
json!({ json!({
@ -4694,10 +4690,10 @@ async fn test_references(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root", "/root",
json!({ json!({
@ -4790,7 +4786,7 @@ async fn test_project_search(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root", "/root",
json!({ json!({
@ -4876,7 +4872,7 @@ async fn test_document_highlights(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-1", "/root-1",
json!({ json!({
@ -4895,7 +4891,7 @@ async fn test_document_highlights(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
let project_id = active_call_a let project_id = active_call_a
@ -4982,7 +4978,7 @@ async fn test_lsp_hover(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root-1", "/root-1",
json!({ json!({
@ -5001,7 +4997,7 @@ async fn test_lsp_hover(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
let project_id = active_call_a let project_id = active_call_a
@ -5100,10 +5096,10 @@ async fn test_project_symbols(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/code", "/code",
json!({ json!({
@ -5211,10 +5207,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/root", "/root",
json!({ json!({
@ -5271,6 +5267,7 @@ async fn test_collaborating_with_code_actions(
deterministic.forbid_parking(); deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
//
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
server server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
@ -5289,10 +5286,10 @@ async fn test_collaborating_with_code_actions(
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -5309,7 +5306,8 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B. // Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let window_b =
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
let workspace_b = window_b.root(cx_b); let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
@ -5515,10 +5513,10 @@ async fn test_collaborating_with_renames(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -5534,7 +5532,8 @@ async fn test_collaborating_with_renames(
.unwrap(); .unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let window_b =
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
let workspace_b = window_b.root(cx_b); let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
@ -5702,10 +5701,10 @@ async fn test_language_server_statuses(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/dir", "/dir",
json!({ json!({
@ -6162,7 +6161,7 @@ async fn test_contacts(
// Test removing a contact // Test removing a contact
client_b client_b
.user_store .user_store()
.update(cx_b, |store, cx| { .update(cx_b, |store, cx| {
store.remove_contact(client_c.user_id().unwrap(), cx) store.remove_contact(client_c.user_id().unwrap(), cx)
}) })
@ -6185,7 +6184,7 @@ async fn test_contacts(
client: &TestClient, client: &TestClient,
cx: &TestAppContext, cx: &TestAppContext,
) -> Vec<(String, &'static str, &'static str)> { ) -> Vec<(String, &'static str, &'static str)> {
client.user_store.read_with(cx, |store, _| { client.user_store().read_with(cx, |store, _| {
store store
.contacts() .contacts()
.iter() .iter()
@ -6228,14 +6227,14 @@ async fn test_contact_requests(
// User A and User C request that user B become their contact. // User A and User C request that user B become their contact.
client_a client_a
.user_store .user_store()
.update(cx_a, |store, cx| { .update(cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap(), cx) store.request_contact(client_b.user_id().unwrap(), cx)
}) })
.await .await
.unwrap(); .unwrap();
client_c client_c
.user_store .user_store()
.update(cx_c, |store, cx| { .update(cx_c, |store, cx| {
store.request_contact(client_b.user_id().unwrap(), cx) store.request_contact(client_b.user_id().unwrap(), cx)
}) })
@ -6289,7 +6288,7 @@ async fn test_contact_requests(
// User B accepts the request from user A. // User B accepts the request from user A.
client_b client_b
.user_store .user_store()
.update(cx_b, |store, cx| { .update(cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
}) })
@ -6333,7 +6332,7 @@ async fn test_contact_requests(
// User B rejects the request from user C. // User B rejects the request from user C.
client_b client_b
.user_store .user_store()
.update(cx_b, |store, cx| { .update(cx_b, |store, cx| {
store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx) store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
}) })
@ -6415,7 +6414,7 @@ async fn test_basic_following(
cx_b.update(editor::init); cx_b.update(editor::init);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -6978,7 +6977,7 @@ async fn test_join_call_after_screen_was_shared(
.await .await
.unwrap(); .unwrap();
client_b.user_store.update(cx_b, |user_store, _| { client_b.user_store().update(cx_b, |user_store, _| {
user_store.clear_cache(); user_store.clear_cache();
}); });
@ -7038,7 +7037,7 @@ async fn test_following_tab_order(
cx_b.update(editor::init); cx_b.update(editor::init);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -7161,7 +7160,7 @@ async fn test_peers_following_each_other(
// Client A shares a project. // Client A shares a project.
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -7334,7 +7333,7 @@ async fn test_auto_unfollowing(
// Client A shares a project. // Client A shares a project.
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -7498,7 +7497,7 @@ async fn test_peers_simultaneously_following_each_other(
cx_a.update(editor::init); cx_a.update(editor::init);
cx_b.update(editor::init); cx_b.update(editor::init);
client_a.fs.insert_tree("/a", json!({})).await; client_a.fs().insert_tree("/a", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let project_id = active_call_a let project_id = active_call_a
@ -7575,10 +7574,10 @@ async fn test_on_input_format_from_host_to_guest(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -7704,10 +7703,10 @@ async fn test_on_input_format_from_guest_to_host(
..Default::default() ..Default::default()
})) }))
.await; .await;
client_a.language_registry.add(Arc::new(language)); client_a.language_registry().add(Arc::new(language));
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
@ -7860,15 +7859,15 @@ async fn test_mutual_editor_inlay_hint_cache_update(
})) }))
.await; .await;
let language = Arc::new(language); let language = Arc::new(language);
client_a.language_registry.add(Arc::clone(&language)); client_a.language_registry().add(Arc::clone(&language));
client_b.language_registry.add(language); client_b.language_registry().add(language);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file", "other.rs": "// Test file",
}), }),
) )
@ -8170,15 +8169,15 @@ async fn test_inlay_hint_refresh_is_forwarded(
})) }))
.await; .await;
let language = Arc::new(language); let language = Arc::new(language);
client_a.language_registry.add(Arc::clone(&language)); client_a.language_registry().add(Arc::clone(&language));
client_b.language_registry.add(language); client_b.language_registry().add(language);
client_a client_a
.fs .fs()
.insert_tree( .insert_tree(
"/a", "/a",
json!({ json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file", "other.rs": "// Test file",
}), }),
) )
@ -8324,30 +8323,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
}); });
} }
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}
fn extract_hint_labels(editor: &Editor) -> Vec<String> { fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new(); let mut labels = Vec::new();
for hint in editor.inlay_hint_cache().hints() { for hint in editor.inlay_hint_cache().hints() {

View file

@ -396,9 +396,9 @@ async fn apply_client_operation(
); );
let root_path = Path::new("/").join(&first_root_name); let root_path = Path::new("/").join(&first_root_name);
client.fs.create_dir(&root_path).await.unwrap(); client.fs().create_dir(&root_path).await.unwrap();
client client
.fs .fs()
.create_file(&root_path.join("main.rs"), Default::default()) .create_file(&root_path.join("main.rs"), Default::default())
.await .await
.unwrap(); .unwrap();
@ -422,8 +422,8 @@ async fn apply_client_operation(
); );
ensure_project_shared(&project, client, cx).await; ensure_project_shared(&project, client, cx).await;
if !client.fs.paths(false).contains(&new_root_path) { if !client.fs().paths(false).contains(&new_root_path) {
client.fs.create_dir(&new_root_path).await.unwrap(); client.fs().create_dir(&new_root_path).await.unwrap();
} }
project project
.update(cx, |project, cx| { .update(cx, |project, cx| {
@ -475,7 +475,7 @@ async fn apply_client_operation(
Some(room.update(cx, |room, cx| { Some(room.update(cx, |room, cx| {
room.join_project( room.join_project(
project_id, project_id,
client.language_registry.clone(), client.language_registry().clone(),
FakeFs::new(cx.background().clone()), FakeFs::new(cx.background().clone()),
cx, cx,
) )
@ -743,7 +743,7 @@ async fn apply_client_operation(
content, content,
} => { } => {
if !client if !client
.fs .fs()
.directories(false) .directories(false)
.contains(&path.parent().unwrap().to_owned()) .contains(&path.parent().unwrap().to_owned())
{ {
@ -752,14 +752,14 @@ async fn apply_client_operation(
if is_dir { if is_dir {
log::info!("{}: creating dir at {:?}", client.username, path); log::info!("{}: creating dir at {:?}", client.username, path);
client.fs.create_dir(&path).await.unwrap(); client.fs().create_dir(&path).await.unwrap();
} else { } else {
let exists = client.fs.metadata(&path).await?.is_some(); let exists = client.fs().metadata(&path).await?.is_some();
let verb = if exists { "updating" } else { "creating" }; let verb = if exists { "updating" } else { "creating" };
log::info!("{}: {} file at {:?}", verb, client.username, path); log::info!("{}: {} file at {:?}", verb, client.username, path);
client client
.fs .fs()
.save(&path, &content.as_str().into(), fs::LineEnding::Unix) .save(&path, &content.as_str().into(), fs::LineEnding::Unix)
.await .await
.unwrap(); .unwrap();
@ -771,12 +771,12 @@ async fn apply_client_operation(
repo_path, repo_path,
contents, contents,
} => { } => {
if !client.fs.directories(false).contains(&repo_path) { if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable); return Err(TestError::Inapplicable);
} }
for (path, _) in contents.iter() { for (path, _) in contents.iter() {
if !client.fs.files().contains(&repo_path.join(path)) { if !client.fs().files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable); return Err(TestError::Inapplicable);
} }
} }
@ -793,16 +793,16 @@ async fn apply_client_operation(
.iter() .iter()
.map(|(path, contents)| (path.as_path(), contents.clone())) .map(|(path, contents)| (path.as_path(), contents.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() { if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?; client.fs().create_dir(&dot_git_dir).await?;
} }
client.fs.set_index_for_repo(&dot_git_dir, &contents); client.fs().set_index_for_repo(&dot_git_dir, &contents);
} }
GitOperation::WriteGitBranch { GitOperation::WriteGitBranch {
repo_path, repo_path,
new_branch, new_branch,
} => { } => {
if !client.fs.directories(false).contains(&repo_path) { if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable); return Err(TestError::Inapplicable);
} }
@ -814,21 +814,21 @@ async fn apply_client_operation(
); );
let dot_git_dir = repo_path.join(".git"); let dot_git_dir = repo_path.join(".git");
if client.fs.metadata(&dot_git_dir).await?.is_none() { if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?; client.fs().create_dir(&dot_git_dir).await?;
} }
client.fs.set_branch_name(&dot_git_dir, new_branch); client.fs().set_branch_name(&dot_git_dir, new_branch);
} }
GitOperation::WriteGitStatuses { GitOperation::WriteGitStatuses {
repo_path, repo_path,
statuses, statuses,
git_operation, git_operation,
} => { } => {
if !client.fs.directories(false).contains(&repo_path) { if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable); return Err(TestError::Inapplicable);
} }
for (path, _) in statuses.iter() { for (path, _) in statuses.iter() {
if !client.fs.files().contains(&repo_path.join(path)) { if !client.fs().files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable); return Err(TestError::Inapplicable);
} }
} }
@ -847,16 +847,16 @@ async fn apply_client_operation(
.map(|(path, val)| (path.as_path(), val.clone())) .map(|(path, val)| (path.as_path(), val.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() { if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?; client.fs().create_dir(&dot_git_dir).await?;
} }
if git_operation { if git_operation {
client client
.fs .fs()
.set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice()); .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
} else { } else {
client.fs.set_status_for_repo_via_working_copy_change( client.fs().set_status_for_repo_via_working_copy_change(
&dot_git_dir, &dot_git_dir,
statuses.as_slice(), statuses.as_slice(),
); );
@ -1499,7 +1499,7 @@ impl TestPlan {
// Invite a contact to the current call // Invite a contact to the current call
0..=70 => { 0..=70 => {
let available_contacts = let available_contacts =
client.user_store.read_with(cx, |user_store, _| { client.user_store().read_with(cx, |user_store, _| {
user_store user_store
.contacts() .contacts()
.iter() .iter()
@ -1596,7 +1596,7 @@ impl TestPlan {
.choose(&mut self.rng) .choose(&mut self.rng)
.cloned() else { continue }; .cloned() else { continue };
let project_root_name = root_name_for_project(&project, cx); let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs.paths(false); let mut paths = client.fs().paths(false);
paths.remove(0); paths.remove(0);
let new_root_path = if paths.is_empty() || self.rng.gen() { let new_root_path = if paths.is_empty() || self.rng.gen() {
Path::new("/").join(&self.next_root_dir_name(user_id)) Path::new("/").join(&self.next_root_dir_name(user_id))
@ -1776,7 +1776,7 @@ impl TestPlan {
let is_dir = self.rng.gen::<bool>(); let is_dir = self.rng.gen::<bool>();
let content; let content;
let mut path; let mut path;
let dir_paths = client.fs.directories(false); let dir_paths = client.fs().directories(false);
if is_dir { if is_dir {
content = String::new(); content = String::new();
@ -1786,7 +1786,7 @@ impl TestPlan {
content = Alphanumeric.sample_string(&mut self.rng, 16); content = Alphanumeric.sample_string(&mut self.rng, 16);
// Create a new file or overwrite an existing file // Create a new file or overwrite an existing file
let file_paths = client.fs.files(); let file_paths = client.fs().files();
if file_paths.is_empty() || self.rng.gen_bool(0.5) { if file_paths.is_empty() || self.rng.gen_bool(0.5) {
path = dir_paths.choose(&mut self.rng).unwrap().clone(); path = dir_paths.choose(&mut self.rng).unwrap().clone();
path.push(gen_file_name(&mut self.rng)); path.push(gen_file_name(&mut self.rng));
@ -1812,7 +1812,7 @@ impl TestPlan {
client: &TestClient, client: &TestClient,
) -> Vec<PathBuf> { ) -> Vec<PathBuf> {
let mut paths = client let mut paths = client
.fs .fs()
.files() .files()
.into_iter() .into_iter()
.filter(|path| path.starts_with(repo_path)) .filter(|path| path.starts_with(repo_path))
@ -1829,7 +1829,7 @@ impl TestPlan {
} }
let repo_path = client let repo_path = client
.fs .fs()
.directories(false) .directories(false)
.choose(&mut self.rng) .choose(&mut self.rng)
.unwrap() .unwrap()
@ -1928,7 +1928,7 @@ async fn simulate_client(
name: "the-fake-language-server", name: "the-fake-language-server",
capabilities: lsp::LanguageServer::full_capabilities(), capabilities: lsp::LanguageServer::full_capabilities(),
initializer: Some(Box::new({ initializer: Some(Box::new({
let fs = client.fs.clone(); let fs = client.app_state.fs.clone();
move |fake_server: &mut FakeLanguageServer| { move |fake_server: &mut FakeLanguageServer| {
fake_server.handle_request::<lsp::request::Completion, _, _>( fake_server.handle_request::<lsp::request::Completion, _, _>(
|_, _| async move { |_, _| async move {
@ -1973,7 +1973,7 @@ async fn simulate_client(
let background = cx.background(); let background = cx.background();
let mut rng = background.rng(); let mut rng = background.rng();
let count = rng.gen_range::<usize, _>(1..3); let count = rng.gen_range::<usize, _>(1..3);
let files = fs.files(); let files = fs.as_fake().files();
let files = (0..count) let files = (0..count)
.map(|_| files.choose(&mut *rng).unwrap().clone()) .map(|_| files.choose(&mut *rng).unwrap().clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -2023,7 +2023,7 @@ async fn simulate_client(
..Default::default() ..Default::default()
})) }))
.await; .await;
client.language_registry.add(Arc::new(language)); client.app_state.languages.add(Arc::new(language));
while let Some(batch_id) = operation_rx.next().await { while let Some(batch_id) = operation_rx.next().await {
let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break };

View file

@ -23,6 +23,7 @@ test-support = [
[dependencies] [dependencies]
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
db = { path = "../db" }
call = { path = "../call" } call = { path = "../call" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
@ -37,6 +38,7 @@ picker = { path = "../picker" }
project = { path = "../project" } project = { path = "../project" }
recent_projects = {path = "../recent_projects"} recent_projects = {path = "../recent_projects"}
settings = { path = "../settings" } settings = { path = "../settings" }
staff_mode = {path = "../staff_mode"}
theme = { path = "../theme" } theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" } theme_selector = { path = "../theme_selector" }
vcs_menu = { path = "../vcs_menu" } vcs_menu = { path = "../vcs_menu" }
@ -44,10 +46,10 @@ util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"} zed-actions = {path = "../zed-actions"}
anyhow.workspace = true anyhow.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true
schemars.workspace = true
postage.workspace = true postage.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,615 @@
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
actions!(
channel_modal,
[
SelectNextControl,
ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
);
pub fn init(cx: &mut AppContext) {
Picker::<ChannelModalDelegate>::init(cx);
cx.add_action(ChannelModal::toggle_mode);
cx.add_action(ChannelModal::toggle_member_admin);
cx.add_action(ChannelModal::remove_member);
cx.add_action(ChannelModal::dismiss);
}
pub struct ChannelModal {
picker: ViewHandle<Picker<ChannelModalDelegate>>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let picker = cx.add_view(|cx| {
Picker::new(
ChannelModalDelegate {
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
user_store: user_store.clone(),
channel_store: channel_store.clone(),
channel_id,
match_candidates: Vec::new(),
members,
mode,
context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx.view_id(), cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
let has_focus = picker.read(cx).has_focus();
Self {
picker,
channel_store,
channel_id,
has_focus,
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
let mode = match self.picker.read(cx).delegate().mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
})
})
.detach();
}
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ChannelModal {
type Event = PickerEvent;
}
impl View for ChannelModal {
fn ui_name() -> &'static str {
"ChannelModal"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.tabbed_modal;
let mode = self.picker.read(cx).delegate().mode;
let Some(channel) = self
.channel_store
.read(cx)
.channel_for_id(self.channel_id) else {
return Empty::new().into_any()
};
enum InviteMembers {}
enum ManageMembers {}
fn render_mode_button<T: 'static>(
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
let active = mode == current_mode;
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
let contained_text = theme.tab_button.style_for(active, state);
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
this.set_mode(mode, cx);
}
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new(format!("#{}", channel.name), theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
"Invite members",
mode,
theme,
cx,
),
render_mode_button::<ManageMembers>(
Mode::ManageMembers,
"Manage members",
mode,
theme,
cx,
),
]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ChannelModal {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum Mode {
ManageMembers,
InviteMembers,
}
pub struct ChannelModalDelegate {
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.len(),
Mode::InviteMembers => self.matching_users.len(),
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
}));
let matches = cx.background().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
}
Mode::InviteMembers => {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
}
Some(proto::channel_member::Kind::Member) => {}
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, admin) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
let in_manage = matches!(self.mode, Mode::ManageMembers);
let mut result = Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children({
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|| {
Label::new("Invited", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
},
)
})
.with_children(admin.and_then(|admin| {
(in_manage && admin).then(|| {
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
})
}))
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
Svg::new("icons/ellipsis.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Mode::InviteMembers => match request_status {
Some(proto::channel_member::Kind::Member) => Some(
Svg::new("icons/check.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Some(proto::channel_member::Kind::Invitee) => Some(
Svg::new("icons/check.svg")
.with_color(theme.invitee_icon.color)
.constrained()
.with_width(theme.invitee_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.invitee_icon.button_width)
.with_height(theme.invitee_icon.button_width)
.contained()
.with_style(theme.invitee_icon.container),
),
Some(proto::channel_member::Kind::AncestorMember) | None => None,
},
};
svg.map(|svg| svg.aligned().flex_float().into_any())
})
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any();
if selected {
result = Stack::new()
.with_child(result)
.with_child(
ChildView::new(&self.context_menu, cx)
.aligned()
.top()
.right(),
)
.into_any();
}
result
}
}
impl ChannelModalDelegate {
fn member_status(
&self,
user_id: UserId,
cx: &AppContext,
) -> Option<proto::channel_member::Kind> {
self.members
.iter()
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
.or_else(|| {
self.channel_store
.read(cx)
.has_pending_channel_invite(self.channel_id, user_id)
.then_some(proto::channel_member::Kind::Invitee)
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.admin),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
}
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, admin) = self.user_at_index(self.selected_index)?;
let admin = !admin.unwrap_or(false);
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_admin(self.channel_id, user.id, admin, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.admin = admin;
}
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, _) = self.user_at_index(self.selected_index)?;
let user_id = user.id;
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
if *member_ix == ix {
return false;
} else if *member_ix > ix {
*member_ix -= 1;
}
true
})
}
this.selected_index = this
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, false, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
this.delegate_mut().members.push(ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
admin: false,
});
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if user_is_admin {
"Make non-admin"
} else {
"Make admin"
},
ToggleMemberAdmin,
),
],
cx,
)
})
}
}

View file

@ -1,28 +1,132 @@
use client::{ContactRequestStatus, User, UserStore}; use client::{ContactRequestStatus, User, UserStore};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; use gpui::{
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc; use std::sync::Arc;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx); Picker::<ContactFinderDelegate>::init(cx);
cx.add_action(ContactFinder::dismiss)
} }
pub type ContactFinder = Picker<ContactFinderDelegate>; pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
pub fn build_contact_finder( impl ContactFinder {
user_store: ModelHandle<UserStore>, pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
cx: &mut ViewContext<ContactFinder>, let picker = cx.add_view(|cx| {
) -> ContactFinder { Picker::new(
Picker::new( ContactFinderDelegate {
ContactFinderDelegate { user_store,
user_store, potential_contacts: Arc::from([]),
potential_contacts: Arc::from([]), selected_index: 0,
selected_index: 0, },
}, cx,
cx, )
) .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
.with_theme(|theme| theme.contact_finder.picker.clone()) });
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ContactFinder {
type Event = PickerEvent;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.tabbed_modal;
fn render_mode_button(
text: &'static str,
theme: &theme::TabbedModal,
_cx: &mut ViewContext<ContactFinder>,
) -> AnyElement<ContactFinder> {
let contained_text = &theme.tab_button.active_state().default;
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new("Contacts", theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([render_mode_button(
"Invite new contacts",
&theme,
cx,
)]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ContactFinder {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
} }
pub struct ContactFinderDelegate { pub struct ContactFinderDelegate {
@ -97,7 +201,9 @@ impl PickerDelegate for ContactFinderDelegate {
selected: bool, selected: bool,
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = &theme::current(cx); let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.contact_finder;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let user = &self.potential_contacts[ix]; let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user); let request_status = self.user_store.read(cx).contact_request_status(user);
@ -109,12 +215,11 @@ impl PickerDelegate for ContactFinderDelegate {
ContactRequestStatus::RequestAccepted => None, ContactRequestStatus::RequestAccepted => None,
}; };
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button &theme.disabled_contact_button
} else { } else {
&theme.contact_finder.contact_button &theme.contact_button
}; };
let style = theme let style = tabbed_modal
.contact_finder
.picker .picker
.item .item
.in_state(selected) .in_state(selected)
@ -122,14 +227,14 @@ impl PickerDelegate for ContactFinderDelegate {
Flex::row() Flex::row()
.with_children(user.avatar.clone().map(|avatar| { .with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar) Image::from_data(avatar)
.with_style(theme.contact_finder.contact_avatar) .with_style(theme.contact_avatar)
.aligned() .aligned()
.left() .left()
})) }))
.with_child( .with_child(
Label::new(user.github_login.clone(), style.label.clone()) Label::new(user.github_login.clone(), style.label.clone())
.contained() .contained()
.with_style(theme.contact_finder.contact_username) .with_style(theme.contact_username)
.aligned() .aligned()
.left(), .left(),
) )
@ -150,7 +255,7 @@ impl PickerDelegate for ContactFinderDelegate {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained() .constrained()
.with_height(theme.contact_finder.row_height) .with_height(tabbed_modal.row_height)
.into_any() .into_any()
} }
} }

View file

@ -0,0 +1,39 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CollaborationPanelDockPosition {
Left,
Right,
}
#[derive(Deserialize, Debug)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: CollaborationPanelDockPosition,
pub default_width: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct CollaborationPanelSettingsContent {
pub button: Option<bool>,
pub dock: Option<CollaborationPanelDockPosition>,
pub default_width: Option<f32>,
}
impl Setting for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = CollaborationPanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View file

@ -1,12 +1,10 @@
use crate::{ use crate::{
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
ToggleScreenSharing,
}; };
use call::{ActiveCall, ParticipantLocation, Room}; use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{ use gpui::{
actions, actions,
@ -33,7 +31,6 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
actions!( actions!(
collab, collab,
[ [
ToggleContactsMenu,
ToggleUserMenu, ToggleUserMenu,
ToggleProjectMenu, ToggleProjectMenu,
SwitchBranch, SwitchBranch,
@ -43,7 +40,6 @@ actions!(
); );
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::toggle_user_menu); cx.add_action(CollabTitlebarItem::toggle_user_menu);
@ -56,7 +52,6 @@ pub struct CollabTitlebarItem {
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
client: Arc<Client>, client: Arc<Client>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
branch_popover: Option<ViewHandle<BranchList>>, branch_popover: Option<ViewHandle<BranchList>>,
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>, project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
user_menu: ViewHandle<ContextMenu>, user_menu: ViewHandle<ContextMenu>,
@ -95,7 +90,7 @@ impl View for CollabTitlebarItem {
right_container right_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
right_container.add_child(self.render_leave_call(&theme, cx)); right_container.add_child(self.render_leave_call(&theme, cx));
let muted = room.read(cx).is_muted(); let muted = room.read(cx).is_muted(cx);
let speaking = room.read(cx).is_speaking(); let speaking = room.read(cx).is_speaking();
left_container.add_child( left_container.add_child(
self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
@ -109,7 +104,6 @@ impl View for CollabTitlebarItem {
let status = workspace.read(cx).client().status(); let status = workspace.read(cx).client().status();
let status = &*status.borrow(); let status = &*status.borrow();
if matches!(status, client::Status::Connected { .. }) { if matches!(status, client::Status::Connected { .. }) {
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
let avatar = user.as_ref().and_then(|user| user.avatar.clone()); let avatar = user.as_ref().and_then(|user| user.avatar.clone());
right_container.add_child(self.render_user_menu_button(&theme, avatar, cx)); right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
} else { } else {
@ -184,7 +178,6 @@ impl CollabTitlebarItem {
project, project,
user_store, user_store,
client, client,
contacts_popover: None,
user_menu: cx.add_view(|cx| { user_menu: cx.add_view(|cx| {
let view_id = cx.view_id(); let view_id = cx.view_id();
let mut menu = ContextMenu::new(view_id, cx); let mut menu = ContextMenu::new(view_id, cx);
@ -315,9 +308,6 @@ impl CollabTitlebarItem {
} }
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) { fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
if ActiveCall::global(cx).read(cx).room().is_none() {
self.contacts_popover = None;
}
cx.notify(); cx.notify();
} }
@ -337,32 +327,6 @@ impl CollabTitlebarItem {
.log_err(); .log_err();
} }
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
if self.contacts_popover.take().is_none() {
let view = cx.add_view(|cx| {
ContactsPopover::new(
self.project.clone(),
self.user_store.clone(),
self.workspace.clone(),
cx,
)
});
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
cx.notify();
}
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) { pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
self.user_menu.update(cx, |user_menu, cx| { self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(_) = self.user_store.read(cx).current_user() { let items = if let Some(_) = self.user_store.read(cx).current_user() {
@ -390,6 +354,7 @@ impl CollabTitlebarItem {
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx); user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
}); });
} }
fn render_branches_popover_host<'a>( fn render_branches_popover_host<'a>(
&'a self, &'a self,
_theme: &'a theme::Titlebar, _theme: &'a theme::Titlebar,
@ -403,8 +368,8 @@ impl CollabTitlebarItem {
.flex(1., true) .flex(1., true)
.contained() .contained()
.constrained() .constrained()
.with_width(theme.contacts_popover.width) .with_width(theme.titlebar.menu.width)
.with_height(theme.contacts_popover.height) .with_height(theme.titlebar.menu.height)
}) })
.on_click(MouseButton::Left, |_, _, _| {}) .on_click(MouseButton::Left, |_, _, _| {})
.on_down_out(MouseButton::Left, move |_, this, cx| { .on_down_out(MouseButton::Left, move |_, this, cx| {
@ -425,6 +390,7 @@ impl CollabTitlebarItem {
.into_any() .into_any()
}) })
} }
fn render_project_popover_host<'a>( fn render_project_popover_host<'a>(
&'a self, &'a self,
_theme: &'a theme::Titlebar, _theme: &'a theme::Titlebar,
@ -438,8 +404,8 @@ impl CollabTitlebarItem {
.flex(1., true) .flex(1., true)
.contained() .contained()
.constrained() .constrained()
.with_width(theme.contacts_popover.width) .with_width(theme.titlebar.menu.width)
.with_height(theme.contacts_popover.height) .with_height(theme.titlebar.menu.height)
}) })
.on_click(MouseButton::Left, |_, _, _| {}) .on_click(MouseButton::Left, |_, _, _| {})
.on_down_out(MouseButton::Left, move |_, this, cx| { .on_down_out(MouseButton::Left, move |_, this, cx| {
@ -459,6 +425,7 @@ impl CollabTitlebarItem {
.into_any() .into_any()
}) })
} }
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) { pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
if self.branch_popover.take().is_none() { if self.branch_popover.take().is_none() {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
@ -519,79 +486,7 @@ impl CollabTitlebarItem {
} }
cx.notify(); cx.notify();
} }
fn render_toggle_contacts_button(
&self,
theme: &Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let titlebar = &theme.titlebar;
let badge = if self
.user_store
.read(cx)
.incoming_contact_requests()
.is_empty()
{
None
} else {
Some(
Empty::new()
.collapsed()
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.with_margin_top(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.aligned(),
)
};
Stack::new()
.with_child(
MouseEventHandler::new::<ToggleContactsMenu, _>(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.in_state(self.contacts_popover.is_some())
.style_for(state);
Svg::new("icons/radix/person.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_contacts_popover(&Default::default(), cx)
})
.with_tooltip::<ToggleContactsMenu>(
0,
"Show contacts menu",
Some(Box::new(ToggleContactsMenu)),
theme.tooltip.clone(),
cx,
),
)
.with_children(badge)
.with_children(self.render_contacts_popover_host(titlebar, cx))
.into_any()
}
fn render_toggle_screen_sharing_button( fn render_toggle_screen_sharing_button(
&self, &self,
theme: &Theme, theme: &Theme,
@ -649,7 +544,7 @@ impl CollabTitlebarItem {
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let icon; let icon;
let tooltip; let tooltip;
let is_muted = room.read(cx).is_muted(); let is_muted = room.read(cx).is_muted(cx);
if is_muted { if is_muted {
icon = "icons/radix/mic-mute.svg"; icon = "icons/radix/mic-mute.svg";
tooltip = "Unmute microphone"; tooltip = "Unmute microphone";
@ -923,23 +818,6 @@ impl CollabTitlebarItem {
.into_any() .into_any()
} }
fn render_contacts_popover_host<'a>(
&'a self,
_theme: &'a theme::Titlebar,
cx: &'a ViewContext<Self>,
) -> Option<AnyElement<Self>> {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(ChildView::new(popover, cx))
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.right()
.into_any()
})
}
fn render_collaborators( fn render_collaborators(
&self, &self,
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,

View file

@ -1,8 +1,6 @@
pub mod collab_panel;
mod collab_titlebar_item; mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
mod contact_notification; mod contact_notification;
mod contacts_popover;
mod face_pile; mod face_pile;
mod incoming_call_notification; mod incoming_call_notification;
mod notifications; mod notifications;
@ -10,7 +8,7 @@ mod project_shared_notification;
mod sharing_status_indicator; mod sharing_status_indicator;
use call::{ActiveCall, Room}; use call::{ActiveCall, Room};
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{actions, AppContext, Task}; use gpui::{actions, AppContext, Task};
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
@ -24,9 +22,7 @@ actions!(
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
vcs_menu::init(cx); vcs_menu::init(cx);
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
contact_list::init(cx); collab_panel::init(app_state.client.clone(), cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(&app_state, cx); incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx); project_shared_notification::init(&app_state, cx);
sharing_status_indicator::init(cx); sharing_status_indicator::init(cx);
@ -68,7 +64,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
if let Some(room) = call.room().cloned() { if let Some(room) = call.room().cloned() {
let client = call.client(); let client = call.client();
room.update(cx, |room, cx| { room.update(cx, |room, cx| {
if room.is_muted() { if room.is_muted(cx) {
ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx); ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
} else { } else {
ActiveCall::report_call_event_for_room( ActiveCall::report_call_event_for_room(

File diff suppressed because it is too large Load diff

View file

@ -1,137 +0,0 @@
use crate::{
contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList,
};
use client::UserStore;
use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use picker::PickerEvent;
use project::Project;
use workspace::Workspace;
actions!(contacts_popover, [ToggleContactFinder]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ContactsPopover::toggle_contact_finder);
}
pub enum Event {
Dismissed,
}
enum Child {
ContactList(ViewHandle<ContactList>),
ContactFinder(ViewHandle<ContactFinder>),
}
pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
_subscription: Option<gpui::Subscription>,
}
impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(cx.add_view(|cx| {
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
})),
project,
user_store,
workspace,
_subscription: None,
};
this.show_contact_list(String::new(), cx);
this
}
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
}
}
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
let finder = build_contact_finder(self.user_store.clone(), cx);
finder.set_query(editor_text, cx);
finder
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
}
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactList::new(
self.project.clone(),
self.user_store.clone(),
self.workspace.clone(),
cx,
)
.with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
crate::contact_list::Event::ToggleContactFinder => {
this.toggle_contact_finder(&Default::default(), cx)
}
}));
self.child = Child::ContactList(child);
cx.notify();
}
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
};
MouseEventHandler::new::<ContactsPopover, _>(0, cx, |_, _| {
Flex::column()
.with_child(child.flex(1., true))
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
})
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
match &self.child {
Child::ContactList(child) => cx.focus(child),
Child::ContactFinder(child) => cx.focus(child),
}
}
}
}

View file

@ -7,44 +7,48 @@ use gpui::{
}, },
json::ToJson, json::ToJson,
serde_json::{self, json}, serde_json::{self, json},
AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext, AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
}; };
use crate::CollabTitlebarItem; pub(crate) struct FacePile<V: View> {
pub(crate) struct FacePile {
overlap: f32, overlap: f32,
faces: Vec<AnyElement<CollabTitlebarItem>>, faces: Vec<AnyElement<V>>,
} }
impl FacePile { impl<V: View> FacePile<V> {
pub fn new(overlap: f32) -> FacePile { pub fn new(overlap: f32) -> Self {
FacePile { Self {
overlap, overlap,
faces: Vec::new(), faces: Vec::new(),
} }
} }
} }
impl Element<CollabTitlebarItem> for FacePile { impl<V: View> Element<V> for FacePile<V> {
type LayoutState = (); type LayoutState = ();
type PaintState = (); type PaintState = ();
fn layout( fn layout(
&mut self, &mut self,
constraint: gpui::SizeConstraint, constraint: gpui::SizeConstraint,
view: &mut CollabTitlebarItem, view: &mut V,
cx: &mut LayoutContext<CollabTitlebarItem>, cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) { ) -> (Vector2F, Self::LayoutState) {
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
let mut width = 0.; let mut width = 0.;
let mut max_height = 0.;
for face in &mut self.faces { for face in &mut self.faces {
width += face.layout(constraint, view, cx).x(); let layout = face.layout(constraint, view, cx);
width += layout.x();
max_height = f32::max(max_height, layout.y());
} }
width -= self.overlap * self.faces.len().saturating_sub(1) as f32; width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
(Vector2F::new(width, constraint.max.y()), ()) (
Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
(),
)
} }
fn paint( fn paint(
@ -53,8 +57,8 @@ impl Element<CollabTitlebarItem> for FacePile {
bounds: RectF, bounds: RectF,
visible_bounds: RectF, visible_bounds: RectF,
_layout: &mut Self::LayoutState, _layout: &mut Self::LayoutState,
view: &mut CollabTitlebarItem, view: &mut V,
cx: &mut PaintContext<CollabTitlebarItem>, cx: &mut PaintContext<V>,
) -> Self::PaintState { ) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
@ -64,6 +68,7 @@ impl Element<CollabTitlebarItem> for FacePile {
for face in self.faces.iter_mut().rev() { for face in self.faces.iter_mut().rev() {
let size = face.size(); let size = face.size();
origin_x -= size.x(); origin_x -= size.x();
let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
scene.paint_layer(None, |scene| { scene.paint_layer(None, |scene| {
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx); face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
}); });
@ -80,8 +85,8 @@ impl Element<CollabTitlebarItem> for FacePile {
_: RectF, _: RectF,
_: &Self::LayoutState, _: &Self::LayoutState,
_: &Self::PaintState, _: &Self::PaintState,
_: &CollabTitlebarItem, _: &V,
_: &ViewContext<CollabTitlebarItem>, _: &ViewContext<V>,
) -> Option<RectF> { ) -> Option<RectF> {
None None
} }
@ -91,8 +96,8 @@ impl Element<CollabTitlebarItem> for FacePile {
bounds: RectF, bounds: RectF,
_: &Self::LayoutState, _: &Self::LayoutState,
_: &Self::PaintState, _: &Self::PaintState,
_: &CollabTitlebarItem, _: &V,
_: &ViewContext<CollabTitlebarItem>, _: &ViewContext<V>,
) -> serde_json::Value { ) -> serde_json::Value {
json!({ json!({
"type": "FacePile", "type": "FacePile",
@ -101,8 +106,8 @@ impl Element<CollabTitlebarItem> for FacePile {
} }
} }
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile { impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) { fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
self.faces.extend(children); self.faces.extend(children);
} }
} }

View file

@ -105,7 +105,7 @@ impl View for DiagnosticIndicator {
let mut summary_row = Flex::row(); let mut summary_row = Flex::row();
if self.summary.error_count > 0 { if self.summary.error_count > 0 {
summary_row.add_child( summary_row.add_child(
Svg::new("icons/circle_x_mark_16.svg") Svg::new("icons/error.svg")
.with_color(style.icon_color_error) .with_color(style.icon_color_error)
.constrained() .constrained()
.with_width(style.icon_width) .with_width(style.icon_width)
@ -121,7 +121,7 @@ impl View for DiagnosticIndicator {
if self.summary.warning_count > 0 { if self.summary.warning_count > 0 {
summary_row.add_child( summary_row.add_child(
Svg::new("icons/triangle_exclamation_16.svg") Svg::new("icons/warning.svg")
.with_color(style.icon_color_warning) .with_color(style.icon_color_warning)
.constrained() .constrained()
.with_width(style.icon_width) .with_width(style.icon_width)
@ -142,7 +142,7 @@ impl View for DiagnosticIndicator {
if self.summary.error_count == 0 && self.summary.warning_count == 0 { if self.summary.error_count == 0 && self.summary.warning_count == 0 {
summary_row.add_child( summary_row.add_child(
Svg::new("icons/circle_check_16.svg") Svg::new("icons/check_circle.svg")
.with_color(style.icon_color_ok) .with_color(style.icon_color_ok)
.constrained() .constrained()
.with_width(style.icon_width) .with_width(style.icon_width)

View file

@ -302,10 +302,11 @@ actions!(
Hover, Hover,
Format, Format,
ToggleSoftWrap, ToggleSoftWrap,
ToggleInlayHints,
RevealInFinder, RevealInFinder,
CopyPath, CopyPath,
CopyRelativePath, CopyRelativePath,
CopyHighlightJson CopyHighlightJson,
] ]
); );
@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts); cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::toggle_soft_wrap); cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::toggle_inlay_hints);
cx.add_action(Editor::reveal_in_finder); cx.add_action(Editor::reveal_in_finder);
cx.add_action(Editor::copy_path); cx.add_action(Editor::copy_path);
cx.add_action(Editor::copy_relative_path); cx.add_action(Editor::copy_relative_path);
@ -1237,7 +1239,8 @@ enum GotoDefinitionKind {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum InlayRefreshReason { enum InlayHintRefreshReason {
Toggle(bool),
SettingsChange(InlayHintSettings), SettingsChange(InlayHintSettings),
NewLinesShown, NewLinesShown,
BufferEdited(HashSet<Arc<Language>>), BufferEdited(HashSet<Arc<Language>>),
@ -1354,8 +1357,8 @@ impl Editor {
})); }));
} }
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
if let project::Event::RefreshInlays = event { if let project::Event::RefreshInlayHints = event {
editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx); editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
}; };
})); }));
} }
@ -2669,13 +2672,41 @@ impl Editor {
} }
} }
fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) { pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
self.refresh_inlay_hints(
InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
cx,
);
}
pub fn inlay_hints_enabled(&self) -> bool {
self.inlay_hint_cache.enabled
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
if self.project.is_none() || self.mode != EditorMode::Full { if self.project.is_none() || self.mode != EditorMode::Full {
return; return;
} }
let (invalidate_cache, required_languages) = match reason { let (invalidate_cache, required_languages) = match reason {
InlayRefreshReason::SettingsChange(new_settings) => { InlayHintRefreshReason::Toggle(enabled) => {
self.inlay_hint_cache.enabled = enabled;
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.inlay_hint_cache.clear();
self.splice_inlay_hints(
self.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect(),
Vec::new(),
cx,
);
return;
}
}
InlayHintRefreshReason::SettingsChange(new_settings) => {
match self.inlay_hint_cache.update_settings( match self.inlay_hint_cache.update_settings(
&self.buffer, &self.buffer,
new_settings, new_settings,
@ -2693,11 +2724,13 @@ impl Editor {
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
} }
} }
InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
InlayRefreshReason::BufferEdited(buffer_languages) => { InlayHintRefreshReason::BufferEdited(buffer_languages) => {
(InvalidationStrategy::BufferEdited, Some(buffer_languages)) (InvalidationStrategy::BufferEdited, Some(buffer_languages))
} }
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None), InlayHintRefreshReason::RefreshRequested => {
(InvalidationStrategy::RefreshRequested, None)
}
}; };
if let Some(InlaySplice { if let Some(InlaySplice {
@ -2774,6 +2807,7 @@ impl Editor {
self.display_map.update(cx, |display_map, cx| { self.display_map.update(cx, |display_map, cx| {
display_map.splice_inlays(to_remove, to_insert, cx); display_map.splice_inlays(to_remove, to_insert, cx);
}); });
cx.notify();
} }
fn trigger_on_type_formatting( fn trigger_on_type_formatting(
@ -7696,8 +7730,8 @@ impl Editor {
.cloned() .cloned()
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if !languages_affected.is_empty() { if !languages_affected.is_empty() {
self.refresh_inlays( self.refresh_inlay_hints(
InlayRefreshReason::BufferEdited(languages_affected), InlayHintRefreshReason::BufferEdited(languages_affected),
cx, cx,
); );
} }
@ -7735,8 +7769,8 @@ impl Editor {
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) { fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(true, cx); self.refresh_copilot_suggestions(true, cx);
self.refresh_inlays( self.refresh_inlay_hints(
InlayRefreshReason::SettingsChange(inlay_hint_settings( InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(), self.selections.newest_anchor().head(),
&self.buffer.read(cx).snapshot(cx), &self.buffer.read(cx).snapshot(cx),
cx, cx,

View file

@ -24,7 +24,7 @@ pub struct InlayHintCache {
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>, hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
allowed_hint_kinds: HashSet<Option<InlayHintKind>>, allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
version: usize, version: usize,
enabled: bool, pub(super) enabled: bool,
update_tasks: HashMap<ExcerptId, TasksForRanges>, update_tasks: HashMap<ExcerptId, TasksForRanges>,
} }
@ -380,7 +380,7 @@ impl InlayHintCache {
} }
} }
fn clear(&mut self) { pub fn clear(&mut self) {
self.version += 1; self.version += 1;
self.update_tasks.clear(); self.update_tasks.clear();
self.hints.clear(); self.hints.clear();
@ -2001,7 +2001,7 @@ mod tests {
}); });
} }
#[gpui::test] #[gpui::test(iterations = 10)]
async fn test_multiple_excerpts_large_multibuffer( async fn test_multiple_excerpts_large_multibuffer(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
@ -2335,10 +2335,12 @@ mod tests {
all hints should be invalidated and requeried for all of its visible excerpts" all hints should be invalidated and requeried for all of its visible excerpts"
); );
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
assert_eq!(
editor.inlay_hint_cache().version, let current_cache_version = editor.inlay_hint_cache().version;
last_scroll_update_version + expected_layers.len(), let minimum_expected_version = last_scroll_update_version + expected_layers.len();
"Due to every excerpt having one hint, cache should update per new excerpt received" assert!(
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
"Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
); );
}); });
} }
@ -2683,6 +2685,127 @@ all hints should be invalidated and requeried for all of its visible excerpts"
}); });
} }
#[gpui::test]
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().start_waiting();
let lsp_request_count = Arc::new(AtomicU32::new(0));
let closure_lsp_request_count = Arc::clone(&lsp_request_count);
fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
label: lsp::InlayHintLabel::String(i.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["1".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should display inlays after toggle despite them disabled in settings"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(
editor.inlay_hint_cache().version,
1,
"First toggle should be cache's first update"
);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert!(
cached_hint_labels(editor).is_empty(),
"Should clear hints after 2nd toggle"
);
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 2);
});
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["2".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should query LSP hints for the 2nd time after enabling hints in settings"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, 3);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert!(
cached_hint_labels(editor).is_empty(),
"Should clear hints after enabling in settings and a 3rd toggle"
);
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 4);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["3".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, 5);
});
}
pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
@ -2759,6 +2882,12 @@ all hints should be invalidated and requeried for all of its visible excerpts"
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
editor.update(cx, |editor, cx| {
assert!(cached_hint_labels(editor).is_empty());
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 0);
});
("/a/main.rs", editor, fake_server) ("/a/main.rs", editor, fake_server)
} }

View file

@ -19,7 +19,7 @@ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover, hover_popover::hide_hover,
persistence::DB, persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot, Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
ToPoint, ToPoint,
}; };
@ -301,7 +301,7 @@ impl Editor {
cx.spawn(|editor, mut cx| async move { cx.spawn(|editor, mut cx| async move {
editor editor
.update(&mut cx, |editor, cx| { .update(&mut cx, |editor, cx| {
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx) editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
}) })
.ok() .ok()
}) })
@ -333,7 +333,7 @@ impl Editor {
cx, cx,
); );
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
} }
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F { pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

View file

@ -44,7 +44,7 @@ impl View for DeployFeedbackButton {
.in_state(active) .in_state(active)
.style_for(state); .style_for(state);
Svg::new("icons/feedback_16.svg") Svg::new("icons/feedback.svg")
.with_color(style.icon_color) .with_color(style.icon_color)
.constrained() .constrained()
.with_width(style.icon_size) .with_width(style.icon_size)

View file

@ -577,6 +577,14 @@ impl AppContext {
} }
} }
pub fn optional_global<T: 'static>(&self) -> Option<&T> {
if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
Some(global.downcast_ref().unwrap())
} else {
None
}
}
pub fn upgrade(&self) -> App { pub fn upgrade(&self) -> App {
App(self.weak_self.as_ref().unwrap().upgrade().unwrap()) App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
} }

View file

@ -48,6 +48,10 @@ pub trait Element<V: View>: 'static {
type LayoutState; type LayoutState;
type PaintState; type PaintState;
fn view_name(&self) -> &'static str {
V::ui_name()
}
fn layout( fn layout(
&mut self, &mut self,
constraint: SizeConstraint, constraint: SizeConstraint,
@ -182,16 +186,27 @@ pub trait Element<V: View>: 'static {
Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx) Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
} }
fn resizable( /// Uses the the given element to calculate resizes for the given tag
fn provide_resize_bounds<Tag: 'static>(self) -> BoundsProvider<V, Tag>
where
Self: 'static + Sized,
{
BoundsProvider::<_, Tag>::new(self.into_any())
}
/// Calls the given closure with the new size of the element whenever the
/// handle is dragged. This will be calculated in relation to the bounds
/// provided by the given tag
fn resizable<Tag: 'static>(
self, self,
side: HandleSide, side: HandleSide,
size: f32, size: f32,
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>), on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
) -> Resizable<V> ) -> Resizable<V>
where where
Self: 'static + Sized, Self: 'static + Sized,
{ {
Resizable::new(self.into_any(), side, size, on_resize) Resizable::new::<Tag>(self.into_any(), side, size, on_resize)
} }
fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V> fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
@ -272,8 +287,16 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
| ElementState::PostLayout { mut element, .. } | ElementState::PostLayout { mut element, .. }
| ElementState::PostPaint { mut element, .. } => { | ElementState::PostPaint { mut element, .. } => {
let (size, layout) = element.layout(constraint, view, cx); let (size, layout) = element.layout(constraint, view, cx);
debug_assert!(size.x().is_finite()); debug_assert!(
debug_assert!(size.y().is_finite()); size.x().is_finite(),
"Element for {:?} had infinite x size after layout",
element.view_name()
);
debug_assert!(
size.y().is_finite(),
"Element for {:?} had infinite y size after layout",
element.view_name()
);
result = size; result = size;
ElementState::PostLayout { ElementState::PostLayout {

View file

@ -82,6 +82,9 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
view: &V, view: &V,
cx: &ViewContext<V>, cx: &ViewContext<V>,
) -> serde_json::Value { ) -> serde_json::Value {
element.debug(view, cx) serde_json::json!({
"type": "ComponentAdapter",
"child": element.debug(view, cx),
})
} }
} }

View file

@ -1,14 +1,14 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use collections::HashMap;
use pathfinder_geometry::vector::{vec2f, Vector2F}; use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json; use serde_json::json;
use crate::{ use crate::{
geometry::rect::RectF, geometry::rect::RectF,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
scene::MouseDrag, AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View, ViewContext,
SizeConstraint, View, ViewContext,
}; };
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
@ -27,15 +27,6 @@ impl HandleSide {
} }
} }
/// 'before' is in reference to the standard english document ordering of left-to-right
/// then top-to-bottom
fn before_content(self) -> bool {
match self {
HandleSide::Left | HandleSide::Top => true,
HandleSide::Right | HandleSide::Bottom => false,
}
}
fn relevant_component(&self, vector: Vector2F) -> f32 { fn relevant_component(&self, vector: Vector2F) -> f32 {
match self.axis() { match self.axis() {
Axis::Horizontal => vector.x(), Axis::Horizontal => vector.x(),
@ -43,14 +34,6 @@ impl HandleSide {
} }
} }
fn compute_delta(&self, e: MouseDrag) -> f32 {
if self.before_content() {
self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
} else {
self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
}
}
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF { fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
match self { match self {
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
@ -69,21 +52,29 @@ impl HandleSide {
} }
} }
fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
where
{
cx.optional_global::<ProviderMap>()
.and_then(|map| map.0.get(&tag))
}
pub struct Resizable<V: View> { pub struct Resizable<V: View> {
child: AnyElement<V>, child: AnyElement<V>,
tag: TypeTag,
handle_side: HandleSide, handle_side: HandleSide,
handle_size: f32, handle_size: f32,
on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>, on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
} }
const DEFAULT_HANDLE_SIZE: f32 = 4.0; const DEFAULT_HANDLE_SIZE: f32 = 4.0;
impl<V: View> Resizable<V> { impl<V: View> Resizable<V> {
pub fn new( pub fn new<Tag: 'static>(
child: AnyElement<V>, child: AnyElement<V>,
handle_side: HandleSide, handle_side: HandleSide,
size: f32, size: f32,
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>), on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
) -> Self { ) -> Self {
let child = match handle_side.axis() { let child = match handle_side.axis() {
Axis::Horizontal => child.constrained().with_max_width(size), Axis::Horizontal => child.constrained().with_max_width(size),
@ -94,6 +85,7 @@ impl<V: View> Resizable<V> {
Self { Self {
child, child,
handle_side, handle_side,
tag: TypeTag::new::<Tag>(),
handle_size: DEFAULT_HANDLE_SIZE, handle_size: DEFAULT_HANDLE_SIZE,
on_resize: Rc::new(RefCell::new(on_resize)), on_resize: Rc::new(RefCell::new(on_resize)),
} }
@ -139,6 +131,14 @@ impl<V: View> Element<V> for Resizable<V> {
handle_region, handle_region,
) )
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
.on_click(MouseButton::Left, {
let on_resize = self.on_resize.clone();
move |click, v, cx| {
if click.click_count == 2 {
on_resize.borrow_mut()(v, None, cx);
}
}
})
.on_drag(MouseButton::Left, { .on_drag(MouseButton::Left, {
let bounds = bounds.clone(); let bounds = bounds.clone();
let side = self.handle_side; let side = self.handle_side;
@ -146,16 +146,30 @@ impl<V: View> Element<V> for Resizable<V> {
let min_size = side.relevant_component(constraint.min); let min_size = side.relevant_component(constraint.min);
let max_size = side.relevant_component(constraint.max); let max_size = side.relevant_component(constraint.max);
let on_resize = self.on_resize.clone(); let on_resize = self.on_resize.clone();
let tag = self.tag;
move |event, view: &mut V, cx| { move |event, view: &mut V, cx| {
if event.end { if event.end {
return; return;
} }
let new_size = min_size
.max(prev_size + side.compute_delta(event)) let Some((bounds, _)) = get_bounds(tag, cx) else {
.min(max_size) return;
.round(); };
let new_size_raw = match side {
// Handle on top side of element => Element is on bottom
HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(),
// Handle on right side of element => Element is on left
HandleSide::Right => event.position.x() - bounds.lower_left().x(),
// Handle on left side of element => Element is on the right
HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(),
// Handle on bottom side of element => Element is on the top
HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
};
let new_size = min_size.max(new_size_raw).min(max_size).round();
if new_size != prev_size { if new_size != prev_size {
on_resize.borrow_mut()(view, new_size, cx); on_resize.borrow_mut()(view, Some(new_size), cx);
} }
} }
}), }),
@ -201,3 +215,80 @@ impl<V: View> Element<V> for Resizable<V> {
}) })
} }
} }
#[derive(Debug, Default)]
struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
pub struct BoundsProvider<V: View, P> {
child: AnyElement<V>,
phantom: std::marker::PhantomData<P>,
}
impl<V: View, P: 'static> BoundsProvider<V, P> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
phantom: std::marker::PhantomData,
}
}
}
impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: crate::SizeConstraint,
view: &mut V,
cx: &mut crate::LayoutContext<V>,
) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}
fn paint(
&mut self,
scene: &mut crate::SceneBuilder,
bounds: pathfinder_geometry::rect::RectF,
visible_bounds: pathfinder_geometry::rect::RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut crate::PaintContext<V>,
) -> Self::PaintState {
cx.update_default_global::<ProviderMap, _, _>(|map, _| {
map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
});
self.child
.paint(scene, bounds.origin(), visible_bounds, view, cx)
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_: pathfinder_geometry::rect::RectF,
_: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &crate::ViewContext<V>,
) -> Option<pathfinder_geometry::rect::RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &crate::ViewContext<V>,
) -> serde_json::Value {
serde_json::json!({
"type": "Provider",
"providing": format!("{:?}", TypeTag::new::<P>()),
"child": self.child.debug(view, cx),
})
}
}

View file

@ -7,6 +7,7 @@ gpui::actions!(
SelectPrev, SelectPrev,
SelectNext, SelectNext,
SelectFirst, SelectFirst,
SelectLast SelectLast,
ShowContextMenu
] ]
); );

View file

@ -13,6 +13,7 @@ use std::{cmp, sync::Arc};
use util::ResultExt; use util::ResultExt;
use workspace::Modal; use workspace::Modal;
#[derive(Clone, Copy)]
pub enum PickerEvent { pub enum PickerEvent {
Dismiss, Dismiss,
} }

View file

@ -282,7 +282,7 @@ pub enum Event {
new_peer_id: proto::PeerId, new_peer_id: proto::PeerId,
}, },
CollaboratorLeft(proto::PeerId), CollaboratorLeft(proto::PeerId),
RefreshInlays, RefreshInlayHints,
} }
pub enum LanguageServerState { pub enum LanguageServerState {
@ -2872,7 +2872,7 @@ impl Project {
.upgrade(&cx) .upgrade(&cx)
.ok_or_else(|| anyhow!("project dropped"))?; .ok_or_else(|| anyhow!("project dropped"))?;
this.update(&mut cx, |project, cx| { this.update(&mut cx, |project, cx| {
cx.emit(Event::RefreshInlays); cx.emit(Event::RefreshInlayHints);
project.remote_id().map(|project_id| { project.remote_id().map(|project_id| {
project.client.send(proto::RefreshInlayHints { project_id }) project.client.send(proto::RefreshInlayHints { project_id })
}) })
@ -3436,7 +3436,7 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
cx.emit(Event::RefreshInlays); cx.emit(Event::RefreshInlayHints);
status.pending_work.remove(&token); status.pending_work.remove(&token);
cx.notify(); cx.notify();
} }
@ -6810,7 +6810,7 @@ impl Project {
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
this.update(&mut cx, |_, cx| { this.update(&mut cx, |_, cx| {
cx.emit(Event::RefreshInlays); cx.emit(Event::RefreshInlayHints);
}); });
Ok(proto::Ack {}) Ok(proto::Ack {})
} }

View file

@ -1651,30 +1651,14 @@ impl workspace::dock::Panel for ProjectPanel {
.unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width) .unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
} }
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) { fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
self.width = Some(size); self.width = size;
self.serialize(cx); self.serialize(cx);
cx.notify(); cx.notify();
} }
fn should_zoom_in_on_event(_: &Self::Event) -> bool { fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
false Some("icons/project.svg")
}
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
false
}
fn is_zoomed(&self, _: &WindowContext) -> bool {
false
}
fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
fn icon_path(&self) -> &'static str {
"icons/folder_tree_16.svg"
} }
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) { fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
@ -1685,14 +1669,6 @@ impl workspace::dock::Panel for ProjectPanel {
matches!(event, Event::DockPositionChanged) matches!(event, Event::DockPositionChanged)
} }
fn should_activate_on_event(_: &Self::Event) -> bool {
false
}
fn should_close_on_event(_: &Self::Event) -> bool {
false
}
fn has_focus(&self, _: &WindowContext) -> bool { fn has_focus(&self, _: &WindowContext) -> bool {
self.has_focus self.has_focus
} }

View file

@ -0,0 +1,22 @@
[package]
name = "quick_action_bar"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/quick_action_bar.rs"
doctest = false
[dependencies]
editor = { path = "../editor" }
gpui = { path = "../gpui" }
search = { path = "../search" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -0,0 +1,163 @@
use editor::Editor;
use gpui::{
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
platform::{CursorStyle, MouseButton},
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
};
use search::{buffer_search, BufferSearchBar};
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub struct QuickActionBar {
buffer_search_bar: ViewHandle<BufferSearchBar>,
active_item: Option<Box<dyn ItemHandle>>,
_inlay_hints_enabled_subscription: Option<Subscription>,
}
impl QuickActionBar {
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
Self {
buffer_search_bar,
active_item: None,
_inlay_hints_enabled_subscription: None,
}
}
fn active_editor(&self) -> Option<ViewHandle<Editor>> {
self.active_item
.as_ref()
.and_then(|item| item.downcast::<Editor>())
}
}
impl Entity for QuickActionBar {
type Event = ();
}
impl View for QuickActionBar {
fn ui_name() -> &'static str {
"QuickActionsBar"
}
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
let Some(editor) = self.active_editor() else { return Empty::new().into_any(); };
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut bar = Flex::row().with_child(render_quick_action_bar_button(
0,
"icons/inlay_hint.svg",
inlay_hints_enabled,
(
"Toggle Inlay Hints".to_string(),
Some(Box::new(editor::ToggleInlayHints)),
),
cx,
|this, cx| {
if let Some(editor) = this.active_editor() {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
});
}
},
));
if editor.read(cx).buffer().read(cx).is_singleton() {
let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
let search_action = buffer_search::Deploy { focus: true };
bar = bar.with_child(render_quick_action_bar_button(
1,
"icons/magnifying_glass.svg",
search_bar_shown,
(
"Buffer Search".to_string(),
Some(Box::new(search_action.clone())),
),
cx,
move |this, cx| {
this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if search_bar_shown {
buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
} else {
buffer_search_bar.deploy(&search_action, cx);
}
});
},
));
}
bar.into_any()
}
}
fn render_quick_action_bar_button<
F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
>(
index: usize,
icon: &'static str,
toggled: bool,
tooltip: (String, Option<Box<dyn Action>>),
cx: &mut ViewContext<QuickActionBar>,
on_click: F,
) -> AnyElement<QuickActionBar> {
enum QuickActionBarButton {}
let theme = theme::current(cx);
let (tooltip_text, action) = tooltip;
MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
let style = theme
.workspace
.toolbar
.toggleable_tool
.in_state(toggled)
.style_for(mouse_state);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
.with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
.into_any_named("quick action bar button")
}
impl ToolbarItemView for QuickActionBar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
match active_pane_item {
Some(active_item) => {
self.active_item = Some(active_item.boxed_clone());
self._inlay_hints_enabled_subscription.take();
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
if inlay_hints_enabled != new_inlay_hints_enabled {
inlay_hints_enabled = new_inlay_hints_enabled;
cx.notify();
}
}));
}
ToolbarItemLocation::PrimaryRight { flex: None }
}
None => {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}
}

View file

@ -102,17 +102,6 @@ message Envelope {
SearchProject search_project = 80; SearchProject search_project = 80;
SearchProjectResponse search_project_response = 81; SearchProjectResponse search_project_response = 81;
GetChannels get_channels = 82;
GetChannelsResponse get_channels_response = 83;
JoinChannel join_channel = 84;
JoinChannelResponse join_channel_response = 85;
LeaveChannel leave_channel = 86;
SendChannelMessage send_channel_message = 87;
SendChannelMessageResponse send_channel_message_response = 88;
ChannelMessageSent channel_message_sent = 89;
GetChannelMessages get_channel_messages = 90;
GetChannelMessagesResponse get_channel_messages_response = 91;
UpdateContacts update_contacts = 92; UpdateContacts update_contacts = 92;
UpdateInviteInfo update_invite_info = 93; UpdateInviteInfo update_invite_info = 93;
ShowContacts show_contacts = 94; ShowContacts show_contacts = 94;
@ -140,6 +129,19 @@ message Envelope {
InlayHints inlay_hints = 116; InlayHints inlay_hints = 116;
InlayHintsResponse inlay_hints_response = 117; InlayHintsResponse inlay_hints_response = 117;
RefreshInlayHints refresh_inlay_hints = 118; RefreshInlayHints refresh_inlay_hints = 118;
CreateChannel create_channel = 119;
ChannelResponse channel_response = 120;
InviteChannelMember invite_channel_member = 121;
RemoveChannelMember remove_channel_member = 122;
RespondToChannelInvite respond_to_channel_invite = 123;
UpdateChannels update_channels = 124;
JoinChannel join_channel = 125;
RemoveChannel remove_channel = 126;
GetChannelMembers get_channel_members = 127;
GetChannelMembersResponse get_channel_members_response = 128;
SetChannelMemberAdmin set_channel_member_admin = 129;
RenameChannel rename_channel = 130;
} }
} }
@ -174,7 +176,8 @@ message JoinRoom {
message JoinRoomResponse { message JoinRoomResponse {
Room room = 1; Room room = 1;
optional LiveKitConnectionInfo live_kit_connection_info = 2; optional uint64 channel_id = 2;
optional LiveKitConnectionInfo live_kit_connection_info = 3;
} }
message RejoinRoom { message RejoinRoom {
@ -867,25 +870,89 @@ message LspDiskBasedDiagnosticsUpdating {}
message LspDiskBasedDiagnosticsUpdated {} message LspDiskBasedDiagnosticsUpdated {}
message GetChannels {} message UpdateChannels {
message GetChannelsResponse {
repeated Channel channels = 1; repeated Channel channels = 1;
repeated uint64 remove_channels = 2;
repeated Channel channel_invitations = 3;
repeated uint64 remove_channel_invitations = 4;
repeated ChannelParticipants channel_participants = 5;
repeated ChannelPermission channel_permissions = 6;
}
message ChannelPermission {
uint64 channel_id = 1;
bool is_admin = 2;
}
message ChannelParticipants {
uint64 channel_id = 1;
repeated uint64 participant_user_ids = 2;
} }
message JoinChannel { message JoinChannel {
uint64 channel_id = 1; uint64 channel_id = 1;
} }
message JoinChannelResponse { message RemoveChannel {
repeated ChannelMessage messages = 1; uint64 channel_id = 1;
bool done = 2;
} }
message LeaveChannel { message GetChannelMembers {
uint64 channel_id = 1; uint64 channel_id = 1;
} }
message GetChannelMembersResponse {
repeated ChannelMember members = 1;
}
message ChannelMember {
uint64 user_id = 1;
bool admin = 2;
Kind kind = 3;
enum Kind {
Member = 0;
Invitee = 1;
AncestorMember = 2;
}
}
message CreateChannel {
string name = 1;
optional uint64 parent_id = 2;
}
message ChannelResponse {
Channel channel = 1;
}
message InviteChannelMember {
uint64 channel_id = 1;
uint64 user_id = 2;
bool admin = 3;
}
message RemoveChannelMember {
uint64 channel_id = 1;
uint64 user_id = 2;
}
message SetChannelMemberAdmin {
uint64 channel_id = 1;
uint64 user_id = 2;
bool admin = 3;
}
message RenameChannel {
uint64 channel_id = 1;
string name = 2;
}
message RespondToChannelInvite {
uint64 channel_id = 1;
bool accept = 2;
}
message GetUsers { message GetUsers {
repeated uint64 user_ids = 1; repeated uint64 user_ids = 1;
} }
@ -918,31 +985,6 @@ enum ContactRequestResponse {
Dismiss = 3; Dismiss = 3;
} }
message SendChannelMessage {
uint64 channel_id = 1;
string body = 2;
Nonce nonce = 3;
}
message SendChannelMessageResponse {
ChannelMessage message = 1;
}
message ChannelMessageSent {
uint64 channel_id = 1;
ChannelMessage message = 2;
}
message GetChannelMessages {
uint64 channel_id = 1;
uint64 before_message_id = 2;
}
message GetChannelMessagesResponse {
repeated ChannelMessage messages = 1;
bool done = 2;
}
message UpdateContacts { message UpdateContacts {
repeated Contact contacts = 1; repeated Contact contacts = 1;
repeated uint64 remove_contacts = 2; repeated uint64 remove_contacts = 2;
@ -1274,14 +1316,7 @@ message Nonce {
message Channel { message Channel {
uint64 id = 1; uint64 id = 1;
string name = 2; string name = 2;
} optional uint64 parent_id = 3;
message ChannelMessage {
uint64 id = 1;
string body = 2;
uint64 timestamp = 3;
uint64 sender_id = 4;
Nonce nonce = 5;
} }
message Contact { message Contact {

View file

@ -1,3 +1,5 @@
#![allow(non_snake_case)]
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope}; use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_tungstenite::tungstenite::Message as WebSocketMessage; use async_tungstenite::tungstenite::Message as WebSocketMessage;
@ -141,9 +143,10 @@ messages!(
(Call, Foreground), (Call, Foreground),
(CallCanceled, Foreground), (CallCanceled, Foreground),
(CancelCall, Foreground), (CancelCall, Foreground),
(ChannelMessageSent, Foreground),
(CopyProjectEntry, Foreground), (CopyProjectEntry, Foreground),
(CreateBufferForPeer, Foreground), (CreateBufferForPeer, Foreground),
(CreateChannel, Foreground),
(ChannelResponse, Foreground),
(CreateProjectEntry, Foreground), (CreateProjectEntry, Foreground),
(CreateRoom, Foreground), (CreateRoom, Foreground),
(CreateRoomResponse, Foreground), (CreateRoomResponse, Foreground),
@ -156,10 +159,6 @@ messages!(
(FormatBuffers, Foreground), (FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground), (FormatBuffersResponse, Foreground),
(FuzzySearchUsers, Foreground), (FuzzySearchUsers, Foreground),
(GetChannelMessages, Foreground),
(GetChannelMessagesResponse, Foreground),
(GetChannels, Foreground),
(GetChannelsResponse, Foreground),
(GetCodeActions, Background), (GetCodeActions, Background),
(GetCodeActionsResponse, Background), (GetCodeActionsResponse, Background),
(GetHover, Background), (GetHover, Background),
@ -179,14 +178,12 @@ messages!(
(GetUsers, Foreground), (GetUsers, Foreground),
(Hello, Foreground), (Hello, Foreground),
(IncomingCall, Foreground), (IncomingCall, Foreground),
(InviteChannelMember, Foreground),
(UsersResponse, Foreground), (UsersResponse, Foreground),
(JoinChannel, Foreground),
(JoinChannelResponse, Foreground),
(JoinProject, Foreground), (JoinProject, Foreground),
(JoinProjectResponse, Foreground), (JoinProjectResponse, Foreground),
(JoinRoom, Foreground), (JoinRoom, Foreground),
(JoinRoomResponse, Foreground), (JoinRoomResponse, Foreground),
(LeaveChannel, Foreground),
(LeaveProject, Foreground), (LeaveProject, Foreground),
(LeaveRoom, Foreground), (LeaveRoom, Foreground),
(OpenBufferById, Background), (OpenBufferById, Background),
@ -209,18 +206,21 @@ messages!(
(RejoinRoom, Foreground), (RejoinRoom, Foreground),
(RejoinRoomResponse, Foreground), (RejoinRoomResponse, Foreground),
(RemoveContact, Foreground), (RemoveContact, Foreground),
(RemoveChannelMember, Foreground),
(ReloadBuffers, Foreground), (ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground), (ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground), (RemoveProjectCollaborator, Foreground),
(RenameProjectEntry, Foreground), (RenameProjectEntry, Foreground),
(RequestContact, Foreground), (RequestContact, Foreground),
(RespondToContactRequest, Foreground), (RespondToContactRequest, Foreground),
(RespondToChannelInvite, Foreground),
(JoinChannel, Foreground),
(RoomUpdated, Foreground), (RoomUpdated, Foreground),
(SaveBuffer, Foreground), (SaveBuffer, Foreground),
(RenameChannel, Foreground),
(SetChannelMemberAdmin, Foreground),
(SearchProject, Background), (SearchProject, Background),
(SearchProjectResponse, Background), (SearchProjectResponse, Background),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground), (ShareProject, Foreground),
(ShareProjectResponse, Foreground), (ShareProjectResponse, Foreground),
(ShowContacts, Foreground), (ShowContacts, Foreground),
@ -233,6 +233,8 @@ messages!(
(UpdateBuffer, Foreground), (UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground), (UpdateBufferFile, Foreground),
(UpdateContacts, Foreground), (UpdateContacts, Foreground),
(RemoveChannel, Foreground),
(UpdateChannels, Foreground),
(UpdateDiagnosticSummary, Foreground), (UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground), (UpdateFollowers, Foreground),
(UpdateInviteInfo, Foreground), (UpdateInviteInfo, Foreground),
@ -245,6 +247,8 @@ messages!(
(UpdateDiffBase, Foreground), (UpdateDiffBase, Foreground),
(GetPrivateUserInfo, Foreground), (GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground), (GetPrivateUserInfoResponse, Foreground),
(GetChannelMembers, Foreground),
(GetChannelMembersResponse, Foreground)
); );
request_messages!( request_messages!(
@ -258,13 +262,12 @@ request_messages!(
(CopyProjectEntry, ProjectEntryResponse), (CopyProjectEntry, ProjectEntryResponse),
(CreateProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse),
(CreateRoom, CreateRoomResponse), (CreateRoom, CreateRoomResponse),
(CreateChannel, ChannelResponse),
(DeclineCall, Ack), (DeclineCall, Ack),
(DeleteProjectEntry, ProjectEntryResponse), (DeleteProjectEntry, ProjectEntryResponse),
(ExpandProjectEntry, ExpandProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse),
(Follow, FollowResponse), (Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse), (FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse),
(GetCodeActions, GetCodeActionsResponse), (GetCodeActions, GetCodeActionsResponse),
(GetHover, GetHoverResponse), (GetHover, GetHoverResponse),
(GetCompletions, GetCompletionsResponse), (GetCompletions, GetCompletionsResponse),
@ -276,7 +279,7 @@ request_messages!(
(GetProjectSymbols, GetProjectSymbolsResponse), (GetProjectSymbols, GetProjectSymbolsResponse),
(FuzzySearchUsers, UsersResponse), (FuzzySearchUsers, UsersResponse),
(GetUsers, UsersResponse), (GetUsers, UsersResponse),
(JoinChannel, JoinChannelResponse), (InviteChannelMember, Ack),
(JoinProject, JoinProjectResponse), (JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse), (JoinRoom, JoinRoomResponse),
(LeaveRoom, Ack), (LeaveRoom, Ack),
@ -293,12 +296,18 @@ request_messages!(
(RefreshInlayHints, Ack), (RefreshInlayHints, Ack),
(ReloadBuffers, ReloadBuffersResponse), (ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack), (RequestContact, Ack),
(RemoveChannelMember, Ack),
(RemoveContact, Ack), (RemoveContact, Ack),
(RespondToContactRequest, Ack), (RespondToContactRequest, Ack),
(RespondToChannelInvite, Ack),
(SetChannelMemberAdmin, Ack),
(GetChannelMembers, GetChannelMembersResponse),
(JoinChannel, JoinRoomResponse),
(RemoveChannel, Ack),
(RenameProjectEntry, ProjectEntryResponse), (RenameProjectEntry, ProjectEntryResponse),
(RenameChannel, ChannelResponse),
(SaveBuffer, BufferSaved), (SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse), (SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
(ShareProject, ShareProjectResponse), (ShareProject, ShareProjectResponse),
(SynchronizeBuffers, SynchronizeBuffersResponse), (SynchronizeBuffers, SynchronizeBuffersResponse),
(Test, Test), (Test, Test),
@ -361,8 +370,6 @@ entity_messages!(
UpdateDiffBase UpdateDiffBase
); );
entity_messages!(channel_id, ChannelMessageSent);
const KIB: usize = 1024; const KIB: usize = 1024;
const MIB: usize = KIB * 1024; const MIB: usize = KIB * 1024;
const MAX_BUFFER_LEN: usize = MIB; const MAX_BUFFER_LEN: usize = MIB;

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 59; pub const PROTOCOL_VERSION: u32 = 60;

View file

@ -39,7 +39,7 @@ pub enum Event {
} }
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::deploy); cx.add_action(BufferSearchBar::deploy_bar);
cx.add_action(BufferSearchBar::dismiss); cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor); cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::select_next_match); cx.add_action(BufferSearchBar::select_next_match);
@ -403,6 +403,19 @@ impl BufferSearchBar {
cx.notify(); cx.notify();
} }
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
if self.show(cx) {
self.search_suggested(cx);
if deploy.focus {
self.select_query(cx);
cx.focus_self();
}
return true;
}
false
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool { pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() { if self.active_searchable_item.is_none() {
return false; return false;
@ -532,21 +545,16 @@ impl BufferSearchBar {
let _ = self.update_matches(cx); let _ = self.update_matches(cx);
cx.notify(); cx.notify();
} }
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
let mut propagate_action = true; let mut propagate_action = true;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| { search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) { if search_bar.deploy(action, cx) {
search_bar.search_suggested(cx);
if action.focus {
search_bar.select_query(cx);
cx.focus_self();
}
propagate_action = false; propagate_action = false;
} }
}); });
} }
if propagate_action { if propagate_action {
cx.propagate_action(); cx.propagate_action();
} }

View file

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

View file

@ -400,7 +400,8 @@ impl TerminalElement {
region = region region = region
// Start selections // Start selections
.on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
cx.focus_parent(); let terminal_view = cx.handle();
cx.focus(&terminal_view);
v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
if let Some(conn_handle) = connection.upgrade(cx) { if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, cx| { conn_handle.update(cx, |terminal, cx| {

View file

@ -362,10 +362,10 @@ impl Panel for TerminalPanel {
} }
} }
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) { fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
match self.position(cx) { match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = Some(size), DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = Some(size), DockPosition::Bottom => self.height = size,
} }
self.serialize(cx); self.serialize(cx);
cx.notify(); cx.notify();
@ -393,8 +393,8 @@ impl Panel for TerminalPanel {
} }
} }
fn icon_path(&self) -> &'static str { fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
"icons/terminal_12.svg" Some("icons/terminal.svg")
} }
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) { fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

Some files were not shown because too many files have changed in this diff Show more