Compare commits
171 Commits
markdown-f
...
gpui3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c43ec918e | ||
|
|
1e61989790 | ||
|
|
c3e5bc767a | ||
|
|
cf8c356d27 | ||
|
|
3da7e7195e | ||
|
|
10f88c841c | ||
|
|
eb4f81abd8 | ||
|
|
cd8fdeb15b | ||
|
|
72d264063f | ||
|
|
77e2816265 | ||
|
|
49cc0b8d68 | ||
|
|
7344dbbe2d | ||
|
|
c345532126 | ||
|
|
67870c8d53 | ||
|
|
edbb526401 | ||
|
|
5987d8b78d | ||
|
|
933157bae8 | ||
|
|
d4dc607ac2 | ||
|
|
fdb8e92178 | ||
|
|
8d61c42a8f | ||
|
|
d0984600f7 | ||
|
|
be776138b3 | ||
|
|
0240f32099 | ||
|
|
7ac00f30fa | ||
|
|
282ad91336 | ||
|
|
857000f535 | ||
|
|
6d36e6f27a | ||
|
|
33fbfb83e5 | ||
|
|
299852cc7d | ||
|
|
7db32fbdf4 | ||
|
|
54d59981da | ||
|
|
714af18afc | ||
|
|
906a2d9b9b | ||
|
|
ac07b9197a | ||
|
|
4b93a5ca44 | ||
|
|
c5b6d78d5b | ||
|
|
eb3d3eaebf | ||
|
|
fdc7751457 | ||
|
|
f561a91daf | ||
|
|
14ba4a9c94 | ||
|
|
fa7dddd6b5 | ||
|
|
4d22a07a1e | ||
|
|
9e287b33e5 | ||
|
|
9d44ed0894 | ||
|
|
21a6664cf8 | ||
|
|
e019d1405a | ||
|
|
e5374f5d7d | ||
|
|
de939e718a | ||
|
|
7d80d1208c | ||
|
|
78ca297282 | ||
|
|
17448f23a6 | ||
|
|
e730a9d029 | ||
|
|
5142e38d2b | ||
|
|
7a1a7929bd | ||
|
|
0368fff030 | ||
|
|
99c31816c9 | ||
|
|
feb2d85a13 | ||
|
|
d6e11c58db | ||
|
|
8a6c2bb749 | ||
|
|
b4f59284a9 | ||
|
|
bffdc55d63 | ||
|
|
9ca0d99cfd | ||
|
|
e5251f4091 | ||
|
|
304158ed79 | ||
|
|
e8f0ebc881 | ||
|
|
7b1d1bf79e | ||
|
|
4b16b73f80 | ||
|
|
7e40addb5f | ||
|
|
f6b5e1734e | ||
|
|
cf4e847c62 | ||
|
|
aff17322f3 | ||
|
|
28650b2fac | ||
|
|
6a4cd53fd8 | ||
|
|
c244416361 | ||
|
|
0511768b22 | ||
|
|
c8b3c4c6cd | ||
|
|
1efd165ead | ||
|
|
787c75cbda | ||
|
|
2d43ad12e6 | ||
|
|
5607cf85c5 | ||
|
|
6ebd6c2893 | ||
|
|
92dea066dd | ||
|
|
7335f211fd | ||
|
|
78fea0dd8e | ||
|
|
9487fffc55 | ||
|
|
b9250d08f9 | ||
|
|
6a5a9c507c | ||
|
|
b9c390c22e | ||
|
|
31c976d8d9 | ||
|
|
5b169fa535 | ||
|
|
a2115e7242 | ||
|
|
31796171de | ||
|
|
a30ea2fc68 | ||
|
|
55ecb3c51b | ||
|
|
8d18dfa4c1 | ||
|
|
f0fac41ca4 | ||
|
|
0bde0f8e2f | ||
|
|
44264ffedc | ||
|
|
7cfc972df6 | ||
|
|
fee0624299 | ||
|
|
cf781dff71 | ||
|
|
706372fe4e | ||
|
|
5948ea217b | ||
|
|
207eb51df1 | ||
|
|
0ee99c6d9c | ||
|
|
d8732adfb2 | ||
|
|
196fd65601 | ||
|
|
e231321655 | ||
|
|
8f08787cf0 | ||
|
|
c5d15fd065 | ||
|
|
ce5f492404 | ||
|
|
3019960f83 | ||
|
|
9f459ba573 | ||
|
|
ecaf44511c | ||
|
|
dc32ab25a0 | ||
|
|
aca23da971 | ||
|
|
db34f29300 | ||
|
|
1fccda7b8d | ||
|
|
463c99b503 | ||
|
|
88b0d3c78e | ||
|
|
165d50ff5b | ||
|
|
731e6d31f6 | ||
|
|
b28287ce91 | ||
|
|
492ca219d3 | ||
|
|
afb253b406 | ||
|
|
41a973b13f | ||
|
|
75c9dc179b | ||
|
|
c443307c19 | ||
|
|
1a90642088 | ||
|
|
2dd5138988 | ||
|
|
a464474df0 | ||
|
|
a0f2c0799e | ||
|
|
1270ef3ea5 | ||
|
|
a76cd778c4 | ||
|
|
a8c7e61021 | ||
|
|
2b143784da | ||
|
|
b53b2c0376 | ||
|
|
e1c509e0de | ||
|
|
f4dbcb6714 | ||
|
|
579bc8f015 | ||
|
|
7c994cd4a5 | ||
|
|
fe59d983eb | ||
|
|
5ef8df22ac | ||
|
|
35407dcf8b | ||
|
|
1ded8e4b8c | ||
|
|
dd32313cd0 | ||
|
|
c55123afa6 | ||
|
|
d0cb78d09d | ||
|
|
1b82ba050f | ||
|
|
864fe2a306 | ||
|
|
3d53f6da1d | ||
|
|
d7e3b10776 | ||
|
|
e6eeac8422 | ||
|
|
6a705fda07 | ||
|
|
76e6c7f6e3 | ||
|
|
1491ca7dcb | ||
|
|
c842988c15 | ||
|
|
f33adc1a7a | ||
|
|
ad50231829 | ||
|
|
fc152eb459 | ||
|
|
c404896a32 | ||
|
|
34d5f264b4 | ||
|
|
cf818948dd | ||
|
|
1122e106a9 | ||
|
|
cb729c4c7f | ||
|
|
e28104b2e8 | ||
|
|
a7159de184 | ||
|
|
3144490fac | ||
|
|
561660ac07 | ||
|
|
d4d6be3f8d | ||
|
|
368ff811b9 |
@@ -13,6 +13,12 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-Objc -all_load"]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-Objc -all_load"]
|
||||
|
||||
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["--cfg", "windows_slim_errors"]
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -113,6 +113,12 @@ jobs:
|
||||
script/check-licenses
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
@@ -123,7 +129,9 @@ jobs:
|
||||
run: |
|
||||
cargo build --workspace --bins --all-features
|
||||
cargo check -p gpui --features "macos-blade"
|
||||
cargo check -p workspace
|
||||
cargo build -p remote_server
|
||||
script/check-rust-livekit-macos
|
||||
|
||||
linux_tests:
|
||||
timeout-minutes: 60
|
||||
@@ -155,8 +163,10 @@ jobs:
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
- name: Build other binaries and features
|
||||
run: |
|
||||
cargo build -p zed
|
||||
cargo check -p workspace
|
||||
|
||||
build_remote_server:
|
||||
timeout-minutes: 60
|
||||
|
||||
8
.github/workflows/deploy_cloudflare.yml
vendored
8
.github/workflows/deploy_cloudflare.yml
vendored
@@ -37,28 +37,28 @@ jobs:
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
|
||||
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
|
||||
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
|
||||
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
|
||||
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier",
|
||||
"format_on_save": "on"
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "prettier",
|
||||
|
||||
762
Cargo.lock
generated
762
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -65,8 +65,9 @@ members = [
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/livekit_client",
|
||||
"crates/livekit_client_macos",
|
||||
"crates/livekit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
"crates/markdown_preview",
|
||||
@@ -248,8 +249,9 @@ language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
live_kit_client = { path = "crates/live_kit_client" }
|
||||
live_kit_server = { path = "crates/live_kit_server" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
livekit_client_macos = { path = "crates/livekit_client_macos" }
|
||||
livekit_server = { path = "crates/livekit_server" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
@@ -382,20 +384,23 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
hyper = "0.14"
|
||||
http = "1.1"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { version = "0.3.0" }
|
||||
jupyter-websocket-client = { version = "0.5.0" }
|
||||
jupyter-protocol = { version = "0.5.0" }
|
||||
jupyter-websocket-client = { version = "0.8.0" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.7.0" }
|
||||
nbformat = { version = "0.9.0" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
@@ -429,7 +434,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.22.0", default-features = false, features = [
|
||||
runtimelib = { version = "0.24.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
@@ -501,7 +506,7 @@ unindent = "0.1.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-script = "0.5.7"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
wasmparser = "0.215"
|
||||
wasm-encoder = "0.215"
|
||||
wasmtime = { version = "24", default-features = false, features = [
|
||||
@@ -570,6 +575,10 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
# TODO livekit https://github.com/RustAudio/cpal/pull/891
|
||||
[patch.crates-io]
|
||||
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
@@ -672,6 +681,7 @@ new_ret_no_self = { level = "allow" }
|
||||
# We have a few `next` functions that differ in lifetimes
|
||||
# compared to Iterator::next. Yet, clippy complains about those.
|
||||
should_implement_trait = { level = "allow" }
|
||||
let_underscore_future = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.20721 10.8551C7.67694 10.8551 6.33401 11.0424 5.52243 11.1878C5.15088 11.2543 4.808 10.9114 4.87454 10.5399C5.01987 9.72825 5.20721 8.38532 5.20721 6.85505C5.20721 5.69375 5.09932 4.64034 4.98377 3.84516C4.9431 3.56522 5.40267 3.3722 5.5684 3.60145C6.30333 4.61809 7.44022 6.08806 8.70721 7.35505C9.9742 8.62204 11.4442 9.75893 12.4608 10.4939C12.69 10.6596 12.497 11.1192 12.2171 11.0785C11.4219 10.963 10.3685 10.8551 9.20721 10.8551Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.11129 10.6816L5.28324 8.85355C5.08798 8.65829 4.7714 8.65829 4.57613 8.85355L3.35355 10.0761C3.15829 10.2714 3.15829 10.588 3.35355 10.7832L5.1816 12.6113C5.37686 12.8066 5.69345 12.8066 5.88871 12.6113L7.11129 11.3887C7.30655 11.1934 7.30655 10.8769 7.11129 10.6816Z" fill="black"/>
|
||||
<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 946 B After Width: | Height: | Size: 751 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4L4 12H12L8 4Z" fill="currentColor"/>
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5 3L3 12H14L8.5 3Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 150 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 4.5L12 11.5M12 4.5L5 11.5" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 199 B |
@@ -108,7 +108,9 @@
|
||||
"ctrl-'": "editor::ToggleHunkDiff",
|
||||
"ctrl-\"": "editor::ExpandAllHunkDiffs",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame"
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
@@ -40,6 +41,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"backspace": "editor::Backspace",
|
||||
@@ -93,8 +95,6 @@
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
@@ -133,6 +133,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
@@ -150,20 +151,23 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"alt-tab": "editor::NextInlineCompletion",
|
||||
"alt-shift-tab": "editor::PreviousInlineCompletion",
|
||||
"ctrl-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-\\": "editor::ShowInlineCompletion"
|
||||
"alt-tab": "editor::ShowInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
@@ -172,12 +176,14 @@
|
||||
},
|
||||
{
|
||||
"context": "Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
@@ -185,6 +191,7 @@
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k c": "assistant::CopyCode",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
@@ -197,6 +204,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-shift-enter": "assistant::Edit",
|
||||
@@ -211,18 +219,22 @@
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread"
|
||||
"cmd-n": "assistant2::NewThread",
|
||||
"cmd-shift-h": "assistant2::OpenHistory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
@@ -231,6 +243,7 @@
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
@@ -244,6 +257,7 @@
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && in_replace > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
@@ -251,6 +265,7 @@
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
@@ -258,6 +273,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
@@ -269,6 +285,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
@@ -276,6 +293,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
@@ -283,6 +301,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
@@ -293,6 +312,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
@@ -321,6 +341,7 @@
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-[": "editor::Outdent",
|
||||
"cmd-]": "editor::Indent",
|
||||
@@ -384,6 +405,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
@@ -391,6 +413,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-1": ["pane::ActivateItem", 0],
|
||||
"ctrl-2": ["pane::ActivateItem", 1],
|
||||
@@ -410,6 +433,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
||||
@@ -465,6 +489,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Workspace && !Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
@@ -475,6 +500,7 @@
|
||||
// Bindings from Sublime Text
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -494,6 +520,7 @@
|
||||
// Bindings from Atom
|
||||
{
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k up": "pane::SplitUp",
|
||||
"cmd-k down": "pane::SplitDown",
|
||||
@@ -504,12 +531,14 @@
|
||||
// Bindings that should be unified with bindings for more general actions
|
||||
{
|
||||
"context": "Editor && renaming",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmRename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
@@ -517,18 +546,21 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_code_actions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCodeAction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "editor::ContextMenuPrev",
|
||||
"ctrl-p": "editor::ContextMenuPrev",
|
||||
@@ -540,6 +572,7 @@
|
||||
},
|
||||
// Custom bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
@@ -550,6 +583,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
@@ -561,6 +595,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProposedChangesEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-y": "editor::ApplyDiffHunk",
|
||||
"cmd-shift-a": "editor::ApplyAllDiffHunks"
|
||||
@@ -568,6 +603,7 @@
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist"
|
||||
@@ -575,12 +611,14 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
@@ -597,6 +635,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
@@ -626,12 +665,14 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
@@ -639,18 +680,21 @@
|
||||
},
|
||||
{
|
||||
"context": "(CollabPanel && editing) > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"space": "collab_panel::InsertSpace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
@@ -659,18 +703,21 @@
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd": "file_finder::ToggleMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && !menu_open",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-p": "file_finder::SelectPrev",
|
||||
"cmd-j": "pane::SplitDown",
|
||||
@@ -681,6 +728,7 @@
|
||||
},
|
||||
{
|
||||
"context": "FileFinder && menu_open",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"j": "pane::SplitDown",
|
||||
"k": "pane::SplitUp",
|
||||
@@ -690,6 +738,7 @@
|
||||
},
|
||||
{
|
||||
"context": "TabSwitcher",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-up": "menu::SelectPrev",
|
||||
"ctrl-down": "menu::SelectNext",
|
||||
@@ -699,6 +748,7 @@
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||
"cmd-c": "terminal::Copy",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[
|
||||
{
|
||||
"context": "VimControl && !menu",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
@@ -33,6 +32,18 @@
|
||||
"(": "vim::SentenceBackward",
|
||||
")": "vim::SentenceForward",
|
||||
"|": "vim::GoToColumn",
|
||||
"] ]": "vim::NextSectionStart",
|
||||
"] [": "vim::NextSectionEnd",
|
||||
"[ [": "vim::PreviousSectionStart",
|
||||
"[ ]": "vim::PreviousSectionEnd",
|
||||
"] m": "vim::NextMethodStart",
|
||||
"] M": "vim::NextMethodEnd",
|
||||
"[ m": "vim::PreviousMethodStart",
|
||||
"[ M": "vim::PreviousMethodEnd",
|
||||
"[ *": "vim::PreviousComment",
|
||||
"[ /": "vim::PreviousComment",
|
||||
"] *": "vim::NextComment",
|
||||
"] /": "vim::NextComment",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
@@ -55,10 +66,10 @@
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
|
||||
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
|
||||
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
|
||||
@@ -176,7 +187,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
@@ -209,6 +219,7 @@
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"=": ["vim::PushOperator", "AutoIndent"],
|
||||
"g u": ["vim::PushOperator", "Lowercase"],
|
||||
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
||||
"g ~": ["vim::PushOperator", "OppositeCase"],
|
||||
@@ -230,7 +241,6 @@
|
||||
},
|
||||
{
|
||||
"context": "VimControl && VimCount",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand"
|
||||
@@ -238,7 +248,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == visual",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "vim::VisualCommand",
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
@@ -275,6 +284,7 @@
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"=": "vim::AutoIndent",
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
"g c": "vim::ToggleComments",
|
||||
@@ -287,7 +297,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -312,9 +321,25 @@
|
||||
"ctrl-o": "vim::TemporaryNormal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_normal",
|
||||
"bindings": {
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"d": "vim::HelixDelete",
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
"b": "vim::PreviousWordStart",
|
||||
|
||||
"h": "vim::Left",
|
||||
"j": "vim::Down",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ShowCompletions",
|
||||
"ctrl-n": "editor::ShowCompletions"
|
||||
@@ -322,7 +347,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == replace",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -340,7 +364,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == waiting",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
@@ -354,16 +377,15 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == operator",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
"ctrl-[": "vim::ClearOperators",
|
||||
"g c": "vim::Comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
@@ -387,12 +409,13 @@
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument",
|
||||
"i": "vim::IndentObj",
|
||||
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
|
||||
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
|
||||
"f": "vim::Method",
|
||||
"c": "vim::Class"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename", // zed specific
|
||||
@@ -401,7 +424,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == d",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"],
|
||||
@@ -411,7 +433,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
@@ -419,7 +440,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gU",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
@@ -427,7 +447,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g~",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
@@ -435,7 +454,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gq",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g q": "vim::CurrentLine",
|
||||
"q": "vim::CurrentLine",
|
||||
@@ -445,7 +463,6 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == y",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
@@ -453,35 +470,36 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == ys",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == >",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == <",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == eq",
|
||||
"bindings": {
|
||||
"=": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gc",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == literal",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
|
||||
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
|
||||
@@ -525,7 +543,6 @@
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
@@ -533,7 +550,6 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -590,7 +606,6 @@
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"g /": "pane::DeploySearch"
|
||||
@@ -599,7 +614,6 @@
|
||||
{
|
||||
// netrw compatibility
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"%": "project_panel::NewFile",
|
||||
@@ -619,6 +633,12 @@
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "project_panel::OpenWithSystem",
|
||||
"] c": "project_panel::SelectNextGitEntry",
|
||||
"[ c": "project_panel::SelectPrevGitEntry",
|
||||
"] d": "project_panel::SelectNextDiagnostic",
|
||||
"[ d": "project_panel::SelectPrevDiagnostic",
|
||||
"}": "project_panel::SelectNextDirectory",
|
||||
"{": "project_panel::SelectPrevDirectory",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent",
|
||||
@@ -627,7 +647,6 @@
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
|
||||
@@ -567,7 +567,18 @@
|
||||
// "History"
|
||||
// 2. Activate the neighbour tab (prefers the right one, if present)
|
||||
// "Neighbour"
|
||||
"activate_on_close": "history"
|
||||
"activate_on_close": "history",
|
||||
/// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
/// Diagnostics are only shown when file icons are also active.
|
||||
/// This setting only works when can take the following three values:
|
||||
///
|
||||
/// 1. Do not mark any files:
|
||||
/// "off"
|
||||
/// 2. Only mark files with errors:
|
||||
/// "errors"
|
||||
/// 3. Mark files with errors and warnings:
|
||||
/// "all"
|
||||
"show_diagnostics": "off"
|
||||
},
|
||||
// Settings related to preview tabs.
|
||||
"preview_tabs": {
|
||||
@@ -1129,6 +1140,7 @@
|
||||
"use_system_clipboard": "always",
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false,
|
||||
"highlight_on_yank_duration": 200,
|
||||
"custom_digraphs": {}
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
@@ -1186,6 +1198,8 @@
|
||||
// "W": "workspace::Save"
|
||||
// }
|
||||
"command_aliases": {},
|
||||
// Whether to show user picture in titlebar.
|
||||
"show_user_picture": true,
|
||||
// ssh_connections is an array of ssh connections.
|
||||
// You can configure these from `project: Open Remote` in the command palette.
|
||||
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Andromeda",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#1e2025ff",
|
||||
"search.match_background": "#11a79366",
|
||||
"panel.background": "#21242bff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#10a793ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#f7f7f84c",
|
||||
"scrollbar.thumb.hover_background": "#252931ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Atelier",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#19171cff",
|
||||
"search.match_background": "#576dda66",
|
||||
"panel.background": "#221f26ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#566ddaff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#efecf44c",
|
||||
"scrollbar.thumb.hover_background": "#332f38ff",
|
||||
@@ -431,7 +431,7 @@
|
||||
"tab.active_background": "#efecf4ff",
|
||||
"search.match_background": "#586dda66",
|
||||
"panel.background": "#e6e3ebff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#586cdaff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#19171c4c",
|
||||
"scrollbar.thumb.hover_background": "#cbc8d1ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Ayu",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#0d1016ff",
|
||||
"search.match_background": "#5ac2fe66",
|
||||
"panel.background": "#1f2127ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#5ac1feff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#bfbdb64c",
|
||||
"scrollbar.thumb.hover_background": "#2d2f34ff",
|
||||
@@ -416,7 +416,7 @@
|
||||
"tab.active_background": "#fcfcfcff",
|
||||
"search.match_background": "#3b9ee566",
|
||||
"panel.background": "#ececedff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#3b9ee5ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#5c61664c",
|
||||
"scrollbar.thumb.hover_background": "#dfe0e1ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Gruvbox",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -55,7 +55,7 @@
|
||||
"tab.active_background": "#282828ff",
|
||||
"search.match_background": "#83a59866",
|
||||
"panel.background": "#3a3735ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#83a598ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
@@ -439,7 +439,7 @@
|
||||
"tab.active_background": "#1d2021ff",
|
||||
"search.match_background": "#83a59866",
|
||||
"panel.background": "#393634ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#83a598ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "One",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Rosé Pine",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#191724ff",
|
||||
"search.match_background": "#57949f66",
|
||||
"panel.background": "#1c1b2aff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#9bced6ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#232132ff",
|
||||
@@ -426,7 +426,7 @@
|
||||
"tab.active_background": "#faf4edff",
|
||||
"search.match_background": "#9cced766",
|
||||
"panel.background": "#fef9f2ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#57949fff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#5752794c",
|
||||
"scrollbar.thumb.hover_background": "#e5e0dfff",
|
||||
@@ -806,7 +806,7 @@
|
||||
"tab.active_background": "#232136ff",
|
||||
"search.match_background": "#9cced766",
|
||||
"panel.background": "#28253cff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#9bced6ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#322f48ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Sandcastle",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#282c33ff",
|
||||
"search.match_background": "#528b8b66",
|
||||
"panel.background": "#2b3038ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#518b8bff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#fdf4c14c",
|
||||
"scrollbar.thumb.hover_background": "#313741ff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Solarized",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#002a35ff",
|
||||
"search.match_background": "#288bd166",
|
||||
"panel.background": "#04313bff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#278ad1ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#fdf6e34c",
|
||||
"scrollbar.thumb.hover_background": "#053541ff",
|
||||
@@ -416,7 +416,7 @@
|
||||
"tab.active_background": "#fdf6e3ff",
|
||||
"search.match_background": "#298bd166",
|
||||
"panel.background": "#f3eddaff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#288bd1ff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#002a354c",
|
||||
"scrollbar.thumb.hover_background": "#dcdacbff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
|
||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
||||
"name": "Summercamp",
|
||||
"author": "Zed Industries",
|
||||
"themes": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"tab.active_background": "#1b1810ff",
|
||||
"search.match_background": "#499bef66",
|
||||
"panel.background": "#231f16ff",
|
||||
"panel.focused_border": null,
|
||||
"panel.focused_border": "#499befff",
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#f8f5de4c",
|
||||
"scrollbar.thumb.hover_background": "#29251bff",
|
||||
|
||||
@@ -3,9 +3,9 @@ use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, AppContext, CursorStyle,
|
||||
EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
@@ -46,34 +46,38 @@ struct PendingWork<'a> {
|
||||
struct Content {
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
on_click:
|
||||
Option<Arc<dyn Fn(&mut ActivityIndicator, &Model<ActivityIndicator>, &mut AppContext)>>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
model.notify(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| model.notify(cx)).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
cx.observe(auto_updater, |_, _, cx| model.notify(cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
@@ -86,7 +90,8 @@ impl ActivityIndicator {
|
||||
|
||||
cx.subscribe(&this, move |_, _, event, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let create_buffer =
|
||||
project.update(cx, |project, model, cx| project.create_buffer(model, cx));
|
||||
let project = project.clone();
|
||||
let error = error.clone();
|
||||
let lsp_name = lsp_name.clone();
|
||||
@@ -105,8 +110,8 @@ impl ActivityIndicator {
|
||||
})?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
Box::new(cx.new_model(|model, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), model, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
@@ -123,29 +128,44 @@ impl ActivityIndicator {
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn show_error_message(
|
||||
&mut self,
|
||||
_: &ShowErrorMessage,
|
||||
model: &Model<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
},
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn dismiss_error_message(
|
||||
&mut self,
|
||||
_: &DismissErrorMessage,
|
||||
model: &Model<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
updater.update(cx, |updater, model, cx| {
|
||||
updater.dismiss_error(model, cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
@@ -183,7 +203,7 @@ impl ActivityIndicator {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
|
||||
fn content_to_render(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
return Some(Content {
|
||||
@@ -194,7 +214,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
this.project.update(cx, |project, model, cx| {
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -352,7 +372,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|indicator, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
indicator.project.update(cx, |project, model, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -442,8 +462,12 @@ impl ActivityIndicator {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu_handle.toggle(cx);
|
||||
fn toggle_language_server_work_context_menu(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.context_menu_handle.toggle(model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,15 +476,20 @@ impl EventEmitter<Event> for ActivityIndicator {}
|
||||
const MAX_MESSAGE_LEN: usize = 50;
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
.on_action(model.listener(Self::show_error_message))
|
||||
.on_action(model.listener(Self::dismiss_error_message));
|
||||
let Some(content) = self.content_to_render(model, cx) else {
|
||||
return result;
|
||||
};
|
||||
let this = cx.view().downgrade();
|
||||
let this = model.downgrade();
|
||||
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
|
||||
result.gap_2().child(
|
||||
PopoverMenu::new("activity-indicator-popover")
|
||||
@@ -480,13 +509,15 @@ impl Render for ActivityIndicator {
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text(&content.message, cx))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(&content.message, cx)
|
||||
})
|
||||
} else {
|
||||
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||
}
|
||||
})
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
this.on_click(model.listener(move |this, _, model, window, cx| {
|
||||
handler(this, cx);
|
||||
}))
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
@@ -494,10 +525,10 @@ impl Render for ActivityIndicator {
|
||||
),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
.menu(move |window, cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
let menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
let menu = ContextMenu::build(window, cx, |mut menu, model, window, cx| {
|
||||
for work in strong_this.read(cx).pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
let this = this.clone();
|
||||
@@ -522,16 +553,17 @@ impl Render for ActivityIndicator {
|
||||
.into_any_element()
|
||||
},
|
||||
move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
this.update(cx, |this, model, cx| {
|
||||
this.project.update(cx, |project, model, cx| {
|
||||
project.cancel_language_server_work(
|
||||
language_server_id,
|
||||
Some(token.clone()),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this.context_menu_handle.hide(cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
@@ -554,5 +586,11 @@ impl Render for ActivityIndicator {
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn ItemHandle>,
|
||||
_: &Model<Self>,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,9 +321,9 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_active_model(&provider_name, &model_id, cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, model, cx| {
|
||||
registry.select_active_model(&provider_name, &model_id, model, cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, model, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{Context as _, IconName, WindowContext};
|
||||
use ui::{Context as _, IconName};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
@@ -51,7 +51,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -59,6 +59,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -70,7 +71,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
let message_2 = context.update(cx, |context, cx| {
|
||||
let message_2 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -83,8 +84,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "1"), (1..1, "2")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
@@ -94,7 +95,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let message_3 = context.update(cx, |context, cx| {
|
||||
let message_3 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -108,7 +109,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let message_4 = context.update(cx, |context, cx| {
|
||||
let message_4 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -123,8 +124,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "C"), (5..5, "D")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
@@ -137,7 +138,9 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Deleting across message boundaries merges the messages.
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(1..4, "")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -147,7 +150,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Undoing the deletion should also undo the merge.
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -159,7 +162,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Redoing the deletion should also redo the merge.
|
||||
buffer.update(cx, |buffer, cx| buffer.redo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.redo(cx));
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -169,7 +172,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Ensure we can still insert after a merged message.
|
||||
let message_5 = context.update(cx, |context, cx| {
|
||||
let message_5 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -193,7 +196,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -201,6 +204,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -212,11 +216,11 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, model, cx)
|
||||
});
|
||||
|
||||
let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
|
||||
let (_, message_2) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
|
||||
let message_2 = message_2.unwrap();
|
||||
|
||||
// We recycle newlines in the middle of a split message
|
||||
@@ -229,7 +233,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
|
||||
let (_, message_3) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
|
||||
let message_3 = message_3.unwrap();
|
||||
|
||||
// We don't recycle newlines at the end of a split message
|
||||
@@ -243,7 +247,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
|
||||
let (_, message_4) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
|
||||
let message_4 = message_4.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
|
||||
assert_eq!(
|
||||
@@ -256,7 +260,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
|
||||
let (_, message_5) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
|
||||
let message_5 = message_5.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
|
||||
assert_eq!(
|
||||
@@ -271,7 +275,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
let (message_6, message_7) =
|
||||
context.update(cx, |context, cx| context.split_message(14..16, cx));
|
||||
context.update(cx, |context, model, cx| context.split_message(14..16, cx));
|
||||
let message_6 = message_6.unwrap();
|
||||
let message_7 = message_7.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
|
||||
@@ -297,7 +301,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -305,6 +309,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -316,20 +321,26 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa")], None, model, cx)
|
||||
});
|
||||
let message_2 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "bbb")], None, model, cx)
|
||||
});
|
||||
|
||||
let message_3 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(8..8, "ccc")], None, model, cx)
|
||||
});
|
||||
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
|
||||
assert_eq!(
|
||||
@@ -351,7 +362,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
let message_4 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -412,7 +423,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -420,6 +431,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -432,7 +444,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
|
||||
context.update(cx, |_, cx| {
|
||||
context.update(cx, |_, model, cx| {
|
||||
cx.subscribe(&context, {
|
||||
let context_ranges = context_ranges.clone();
|
||||
move |context, _, event, _| {
|
||||
@@ -446,7 +458,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||
for range in removed {
|
||||
context_ranges.parsed_commands.remove(range);
|
||||
context_ranges.parsed_commands.remove(&range);
|
||||
}
|
||||
for command in updated {
|
||||
context_ranges
|
||||
@@ -467,7 +479,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
|
||||
// Insert a slash command
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
|
||||
});
|
||||
assert_text_and_context_ranges(
|
||||
@@ -480,7 +492,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Edit the argument of the slash command.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
let edit_offset = buffer.text().find("lib.rs").unwrap();
|
||||
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
|
||||
});
|
||||
@@ -494,7 +506,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Edit the name of the slash command, using one that doesn't exist.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
let edit_offset = buffer.text().find("/file").unwrap();
|
||||
buffer.edit(
|
||||
[(edit_offset..edit_offset + "/file".len(), "/unknown")],
|
||||
@@ -512,7 +524,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Undoing the insertion of an non-existent slash command resorts the previous one.
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_text_and_context_ranges(
|
||||
&buffer,
|
||||
&context_ranges,
|
||||
@@ -523,7 +535,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let (command_output_tx, command_output_rx) = mpsc::unbounded();
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
|
||||
context.insert_command_output(
|
||||
command_source_range,
|
||||
@@ -619,7 +631,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let mut actual_marked_text = String::new();
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.update(cx, |buffer, model, _| {
|
||||
struct Endpoint {
|
||||
offset: usize,
|
||||
marker: char,
|
||||
@@ -703,7 +715,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
|
||||
// Create a new context
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
Some(project),
|
||||
@@ -711,12 +723,13 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let assistant_message_id = context.update(cx, |context, model, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
context
|
||||
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
|
||||
@@ -903,7 +916,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
@@ -932,7 +945,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_patches(
|
||||
@@ -968,7 +981,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
|
||||
// Ensure steps are re-parsed when deserializing.
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new_model(|cx| {
|
||||
let deserialized_context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
@@ -978,6 +991,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1013,9 +1027,14 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
|
||||
context.update(cx, |context, cx| {
|
||||
context.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit_via_marked_text(
|
||||
&new_text_marked_with_edits.unindent(),
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
@@ -1031,7 +1050,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, model, cx| {
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
let ranges = context
|
||||
.patches
|
||||
@@ -1084,7 +1103,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -1092,31 +1111,32 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
|
||||
let message_1 = context.update(cx, |context, cx| {
|
||||
let message_1 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
let message_2 = context.update(cx, |context, cx| {
|
||||
let message_2 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
});
|
||||
let _message_3 = context.update(cx, |context, cx| {
|
||||
let _message_3 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
|
||||
assert_eq!(
|
||||
cx.read(|cx| messages(&context, cx)),
|
||||
@@ -1128,7 +1148,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new_model(|cx| {
|
||||
let deserialized_context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
@@ -1138,6 +1158,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1187,7 +1208,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
i as ReplicaId,
|
||||
@@ -1198,6 +1219,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1232,15 +1254,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
match rng.gen_range(0..100) {
|
||||
0..=29 if mutation_count > 0 => {
|
||||
log::info!("Context {}: edit buffer", context_index);
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.randomly_edit(&mut rng, 1, cx)
|
||||
});
|
||||
});
|
||||
mutation_count -= 1;
|
||||
}
|
||||
30..=44 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let range = context.buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
log::info!("Context {}: split message at {:?}", context_index, range);
|
||||
context.split_message(range, cx);
|
||||
@@ -1248,7 +1270,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
45..=59 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let role = *[Role::User, Role::Assistant, Role::System]
|
||||
.choose(&mut rng)
|
||||
@@ -1265,7 +1287,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
60..=74 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let command_text = "/".to_string()
|
||||
+ slash_commands
|
||||
.command_names()
|
||||
@@ -1274,7 +1296,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
.clone()
|
||||
.as_ref();
|
||||
|
||||
let command_range = context.buffer.update(cx, |buffer, cx| {
|
||||
let command_range = context.buffer.update(cx, |buffer, model, cx| {
|
||||
let offset = buffer.random_byte_range(0, &mut rng).start;
|
||||
buffer.edit(
|
||||
[(offset..offset, format!("\n{}\n", command_text))],
|
||||
@@ -1342,7 +1364,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
75..=84 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let new_status = match rng.gen_range(0..3) {
|
||||
0 => MessageStatus::Done,
|
||||
@@ -1390,7 +1412,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
);
|
||||
|
||||
network.lock().broadcast(replica_id, ops_to_send);
|
||||
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.apply_ops(ops_to_receive, cx)
|
||||
});
|
||||
} else if rng.gen_bool(0.1) && replica_id != 0 {
|
||||
log::info!("Context {}: disconnecting", context_index);
|
||||
network.lock().disconnect_peer(replica_id);
|
||||
@@ -1402,7 +1426,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
.map(ContextOperation::from_proto)
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.unwrap();
|
||||
context.update(cx, |context, cx| context.apply_ops(ops, cx));
|
||||
context.update(cx, |context, model, cx| context.apply_ops(ops, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1449,7 +1473,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -1457,6 +1481,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1471,7 +1496,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
|
||||
@@ -1484,22 +1509,28 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
"Empty messages should not have any cache anchors."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa")], None, model, cx)
|
||||
});
|
||||
let message_2 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "bbbbbbb")], None, model, cx)
|
||||
});
|
||||
let message_3 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(12..12, "cccccc")], None, model, cx)
|
||||
});
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
|
||||
@@ -1511,12 +1542,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
0,
|
||||
"Messages should not be marked for cache before going over the token minimum."
|
||||
);
|
||||
context.update(cx, |context, _| {
|
||||
context.update(cx, |context, model, _| {
|
||||
context.token_count = Some(20);
|
||||
});
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, true, cx)
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, true, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
@@ -1528,12 +1559,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1544,7 +1575,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
vec![false, true, true, false],
|
||||
"Most recent message should also be cached if not a speculative request."
|
||||
);
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.update_cache_status_for_completion(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1563,8 +1594,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
"All user messages prior to anchor should be marked as cached."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(14..14, "d")], None, model, cx)
|
||||
});
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1582,8 +1615,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
],
|
||||
"Modifying a message should invalidate it's cache but leave previous messages."
|
||||
);
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(2..2, "e")], None, model, cx)
|
||||
});
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1642,8 +1677,9 @@ impl SlashCommand for FakeSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![]))
|
||||
}
|
||||
@@ -1657,9 +1693,10 @@ impl SlashCommand for FakeSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text: format!("Executed fake command: {}", self.0),
|
||||
|
||||
@@ -14,9 +14,7 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
@@ -109,11 +107,16 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new_model(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
let context_server_manager = cx.new_model(|model, cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
@@ -127,10 +130,10 @@ impl ContextStore {
|
||||
slash_commands,
|
||||
tools,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
_watch_updates: model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
@@ -148,12 +151,12 @@ impl ContextStore {
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_changed(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.handle_project_changed(project.clone(), model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
this.register_context_server_handlers(model, cx);
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -166,7 +169,7 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.host_contexts = envelope
|
||||
.payload
|
||||
.contexts
|
||||
@@ -176,7 +179,7 @@ impl ContextStore {
|
||||
summary: context.summary,
|
||||
})
|
||||
.collect();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,7 +189,7 @@ impl ContextStore {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
let operations = this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
@@ -215,14 +218,14 @@ impl ContextStore {
|
||||
_: TypedEnvelope<proto::CreateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("can only create contexts as the host"));
|
||||
}
|
||||
|
||||
let context = this.create(cx);
|
||||
let context = this.create(model, cx);
|
||||
let context_id = context.read(cx).id().clone();
|
||||
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
|
||||
model.emit(ContextStoreEvent::ContextCreated(context_id.clone()), cx);
|
||||
|
||||
anyhow::Ok((
|
||||
context_id,
|
||||
@@ -243,12 +246,14 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::UpdateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let operation_proto = envelope.payload.operation.context("invalid operation")?;
|
||||
let operation = ContextOperation::from_proto(operation_proto)?;
|
||||
context.update(cx, |context, cx| context.apply_ops([operation], cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.apply_ops([operation], model, cx)
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
@@ -259,7 +264,7 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::SynchronizeContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
@@ -298,7 +303,12 @@ impl ContextStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
|
||||
fn handle_project_changed(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
@@ -319,7 +329,7 @@ impl ContextStore {
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
|
||||
.map(|subscription| subscription.set_model(model, &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
@@ -330,22 +340,23 @@ impl ContextStore {
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
project::Event::HostReshared | project::Event::Rejoined => {
|
||||
self.synchronize_contexts(cx);
|
||||
self.synchronize_contexts(model, cx);
|
||||
}
|
||||
project::Event::DisconnectedFromHost => {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Weak(context.downgrade());
|
||||
strong_context.update(cx, |context, cx| {
|
||||
strong_context.update(cx, |context, model, cx| {
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
context.set_capability(language::Capability::ReadOnly, cx);
|
||||
context.set_capability(language::Capability::ReadOnly, model, cx);
|
||||
}
|
||||
});
|
||||
true
|
||||
@@ -354,14 +365,14 @@ impl ContextStore {
|
||||
}
|
||||
});
|
||||
self.host_contexts.clear();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||
let context = cx.new_model(|cx| {
|
||||
pub fn create(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Context> {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
self.languages.clone(),
|
||||
Some(self.project.clone()),
|
||||
@@ -369,16 +380,18 @@ impl ContextStore {
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.register_context(&context, cx);
|
||||
self.register_context(&context, model, cx);
|
||||
context
|
||||
}
|
||||
|
||||
pub fn create_remote_context(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
@@ -394,11 +407,11 @@ impl ContextStore {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
let request = self.client.request(proto::CreateContext { project_id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_id = ContextId::from_proto(response.context_id);
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
@@ -409,6 +422,7 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -423,12 +437,12 @@ impl ContextStore {
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context(&context, model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
@@ -438,7 +452,8 @@ impl ContextStore {
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
return Task::ready(Ok(existing_context));
|
||||
@@ -459,9 +474,9 @@ impl ContextStore {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
saved_context,
|
||||
path.clone(),
|
||||
@@ -471,14 +486,15 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.register_context(&context, model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
@@ -514,7 +530,8 @@ impl ContextStore {
|
||||
pub fn open_remote_context(
|
||||
&mut self,
|
||||
context_id: ContextId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
@@ -537,10 +554,10 @@ impl ContextStore {
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
@@ -551,6 +568,7 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -565,19 +583,24 @@ impl ContextStore {
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context(&context, model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
|
||||
fn register_context(
|
||||
&mut self,
|
||||
context: &Model<Context>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let handle = if self.project_is_shared {
|
||||
ContextHandle::Strong(context.clone())
|
||||
} else {
|
||||
@@ -592,7 +615,8 @@ impl ContextStore {
|
||||
&mut self,
|
||||
context: Model<Context>,
|
||||
event: &ContextEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
@@ -651,7 +675,7 @@ impl ContextStore {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn synchronize_contexts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
@@ -674,36 +698,37 @@ impl ContextStore {
|
||||
project_id,
|
||||
contexts,
|
||||
});
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = request.await?;
|
||||
model
|
||||
.spawn(cx, |this, cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
@@ -740,9 +765,9 @@ impl ContextStore {
|
||||
&self.host_contexts
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn reload(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||
@@ -778,14 +803,14 @@ impl ContextStore {
|
||||
}
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn restart_context_servers(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.update_model(
|
||||
&self.context_server_manager,
|
||||
|context_server_manager, cx| {
|
||||
@@ -798,7 +823,7 @@ impl ContextStore {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
|
||||
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
Self::handle_context_server_event,
|
||||
@@ -810,7 +835,8 @@ impl ContextStore {
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let slash_command_working_set = self.slash_commands.clone();
|
||||
let tool_working_set = self.tools.clone();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@ impl ResolvedPatch {
|
||||
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
|
||||
}
|
||||
}
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
@@ -461,7 +461,7 @@ impl AssistantPatch {
|
||||
// Expand the context ranges of each edit and group edits with overlapping context ranges.
|
||||
let mut edit_groups_by_buffer = HashMap::default();
|
||||
for (buffer, edits) in edits_by_buffer {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, model, _| buffer.text_snapshot()) {
|
||||
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
|
||||
}
|
||||
}
|
||||
@@ -918,7 +918,7 @@ mod tests {
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||
let buffer = cx.new_model(|model, cx| Buffer::local(text.clone(), model, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
|
||||
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||
@@ -932,8 +932,9 @@ mod tests {
|
||||
new_text: String,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|model, cx| {
|
||||
Buffer::local(old_text, model, cx).with_language(Arc::new(rust_lang()), model, cx)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let resolved_edits = edits
|
||||
.into_iter()
|
||||
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
use text::LineEnding;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, AppContext, IconButtonShape, KeyBinding, ListItem, ListItemSpacing,
|
||||
ParentElement, Render, SharedString, Styled, Tooltip, VisualContext,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -88,7 +88,7 @@ pub fn open_prompt_library(
|
||||
.find_map(|window| window.downcast::<PromptLibrary>());
|
||||
if let Some(existing_window) = existing_window {
|
||||
existing_window
|
||||
.update(cx, |_, cx| cx.activate_window())
|
||||
.update(cx, |_, model, cx| cx.activate_window())
|
||||
.ok();
|
||||
Task::ready(Ok(existing_window))
|
||||
} else {
|
||||
@@ -109,7 +109,11 @@ pub fn open_prompt_library(
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
|cx| {
|
||||
cx.new_model(|model, cx| {
|
||||
PromptLibrary::new(store, language_registry, model, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
})?
|
||||
})
|
||||
@@ -121,14 +125,14 @@ pub struct PromptLibrary {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_editors: HashMap<PromptId, PromptEditor>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
picker: View<Picker<PromptPickerDelegate>>,
|
||||
picker: Model<Picker<PromptPickerDelegate>>,
|
||||
pending_load: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
title_editor: View<Editor>,
|
||||
body_editor: View<Editor>,
|
||||
title_editor: Model<Editor>,
|
||||
body_editor: Model<Editor>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
next_title_and_body_to_save: Option<(String, Rope)>,
|
||||
@@ -158,7 +162,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
fn no_matches_text(
|
||||
&self,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> SharedString {
|
||||
if self.store.prompt_count() == 0 {
|
||||
"No prompts.".into()
|
||||
} else {
|
||||
@@ -170,23 +178,31 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_index = ix;
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Search...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let (matches, selected_index) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -201,30 +217,34 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
@@ -241,9 +261,9 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.selected(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
.tooltip(move |window, cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
|
||||
}))
|
||||
}))
|
||||
.end_hover_slot(
|
||||
@@ -253,11 +273,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
div()
|
||||
.id("built-in-prompt")
|
||||
.child(Icon::new(IconName::FileLock).color(Color::Muted))
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -266,9 +287,9 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
.tooltip(move |window, cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::Deleted { prompt_id }, cx)
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
@@ -278,7 +299,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
@@ -288,15 +309,20 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
|
||||
})),
|
||||
),
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
fn render_editor(
|
||||
&self,
|
||||
editor: &Model<Editor>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
@@ -313,7 +339,8 @@ impl PromptLibrary {
|
||||
fn new(
|
||||
store: Arc<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
@@ -321,11 +348,11 @@ impl PromptLibrary {
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
let picker = cx.new_model(|model, cx| {
|
||||
let picker = Picker::uniform_list(delegate, model, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker.focus(window, cx);
|
||||
picker
|
||||
});
|
||||
Self {
|
||||
@@ -341,47 +368,52 @@ impl PromptLibrary {
|
||||
|
||||
fn handle_picker_event(
|
||||
&mut self,
|
||||
_: View<Picker<PromptPickerDelegate>>,
|
||||
_: Model<Picker<PromptPickerDelegate>>,
|
||||
event: &PromptPickerEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
self.load_prompt(*prompt_id, false, model, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
self.load_prompt(*prompt_id, true, model, cx);
|
||||
}
|
||||
PromptPickerEvent::ToggledDefault { prompt_id } => {
|
||||
self.toggle_default_for_prompt(*prompt_id, cx);
|
||||
self.toggle_default_for_prompt(*prompt_id, model, cx);
|
||||
}
|
||||
PromptPickerEvent::Deleted { prompt_id } => {
|
||||
self.delete_prompt(*prompt_id, cx);
|
||||
self.delete_prompt(*prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn new_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
// If we already have an untitled prompt, use that instead
|
||||
// of creating a new one.
|
||||
if let Some(metadata) = self.store.first() {
|
||||
if metadata.title.is_none() {
|
||||
self.load_prompt(metadata.id, true, cx);
|
||||
self.load_prompt(metadata.id, true, model, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prompt_id = PromptId::new();
|
||||
let save = self.store.save(prompt_id, None, false, "".into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.load_prompt(prompt_id, true, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
|
||||
|
||||
if prompt_id.is_built_in() {
|
||||
@@ -391,7 +423,7 @@ impl PromptLibrary {
|
||||
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
|
||||
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
let title = prompt_editor.title_editor.read(cx).text(cx);
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, model, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@@ -407,10 +439,10 @@ impl PromptLibrary {
|
||||
|
||||
prompt_editor.next_title_and_body_to_save = Some((title, body));
|
||||
if prompt_editor.pending_save.is_none() {
|
||||
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
|
||||
prompt_editor.pending_save = Some(model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
loop {
|
||||
let title_and_body = this.update(&mut cx, |this, _| {
|
||||
let title_and_body = this.update(&mut cx, |this, _, _| {
|
||||
this.prompt_editors
|
||||
.get_mut(&prompt_id)?
|
||||
.next_title_and_body_to_save
|
||||
@@ -427,9 +459,10 @@ impl PromptLibrary {
|
||||
.save(prompt_id, title, prompt_metadata.default, body)
|
||||
.await
|
||||
.log_err();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
executor.timer(SAVE_THROTTLE).await;
|
||||
@@ -449,77 +482,90 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.delete_prompt(active_prompt_id, cx);
|
||||
self.delete_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.duplicate_prompt(active_prompt_id, cx);
|
||||
self.duplicate_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.toggle_default_for_prompt(active_prompt_id, cx);
|
||||
self.toggle_default_for_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
self.store
|
||||
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
pub fn load_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
focus: bool,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
if focus {
|
||||
prompt_editor
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, model, cx| editor.focus(window, cx));
|
||||
}
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
self.set_active_prompt(Some(prompt_id), model, cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_load = model.spawn(cx, |this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
this.update(&mut cx, |this, cx| match prompt {
|
||||
this.update(&mut cx, |this, model, cx| match prompt {
|
||||
Ok(prompt) => {
|
||||
let title_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_width(cx);
|
||||
editor.set_placeholder_text("Untitled", cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
let title_editor = cx.new_model(|model, cx| {
|
||||
let mut editor = Editor::auto_width(model, cx);
|
||||
editor.set_placeholder_text("Untitled", model, cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), model, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), model, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
let body_editor = cx.new_view(|cx| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
let body_editor = cx.new_model(|model, cx| {
|
||||
// todo!: Remove this type annotation on cx
|
||||
let buffer = cx.new_model(|model, cx: &mut AppContext| {
|
||||
let mut buffer = Buffer::local(prompt, model, cx);
|
||||
buffer.set_language(markdown.log_err(), model, cx);
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer
|
||||
});
|
||||
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, model, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), model, cx);
|
||||
}
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, model, cx);
|
||||
editor.set_show_gutter(false, model, cx);
|
||||
editor.set_show_wrap_guides(false, model, cx);
|
||||
editor.set_show_indent_guides(false, model, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Some(Box::new(
|
||||
@@ -530,7 +576,7 @@ impl PromptLibrary {
|
||||
),
|
||||
)));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
editor.focus(window, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -567,9 +613,14 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
fn set_active_prompt(
|
||||
&mut self,
|
||||
prompt_id: Option<PromptId>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
self.picker.update(cx, |picker, model, cx| {
|
||||
if let Some(prompt_id) = prompt_id {
|
||||
if picker
|
||||
.delegate
|
||||
@@ -589,13 +640,13 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
picker.focus(cx);
|
||||
picker.focus(window);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
let confirmation = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
@@ -607,25 +658,32 @@ impl PromptLibrary {
|
||||
&["Delete", "Cancel"],
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.set_active_prompt(None, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.set_active_prompt(None, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
|
||||
const DUPLICATE_SUFFIX: &str = " copy";
|
||||
let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
|
||||
@@ -655,31 +713,39 @@ impl PromptLibrary {
|
||||
let save = self
|
||||
.store
|
||||
.save(new_id, Some(title.into()), false, body.into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |prompt_library, cx| {
|
||||
prompt_library.load_prompt(new_id, true, cx)
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |prompt_library, cx| {
|
||||
prompt_library.load_prompt(new_id, true, cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
fn focus_active_prompt(&mut self, _: &Tab, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt) = self.active_prompt_id {
|
||||
self.prompt_editors[&active_prompt]
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, model, cx| editor.focus(window, cx));
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.focus(window));
|
||||
}
|
||||
|
||||
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
pub fn inline_assist(
|
||||
&mut self,
|
||||
action: &InlineAssist,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||
cx.propagate();
|
||||
return;
|
||||
@@ -693,13 +759,13 @@ impl PromptLibrary {
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated(cx) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
cx.activate_window();
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
})
|
||||
@@ -713,7 +779,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
|
||||
fn move_down_from_title(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveDown,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.body_editor);
|
||||
@@ -721,7 +792,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
|
||||
fn move_up_from_body(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveUp,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.title_editor);
|
||||
@@ -732,18 +808,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_title_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
title_editor: View<Editor>,
|
||||
title_editor: Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, model, cx);
|
||||
self.count_tokens(prompt_id, model, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
title_editor.update(cx, |title_editor, cx| {
|
||||
title_editor.change_selections(None, cx, |selections| {
|
||||
title_editor.update(cx, |title_editor, model, cx| {
|
||||
title_editor.change_selections(None, model, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -756,18 +833,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_body_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
body_editor: View<Editor>,
|
||||
body_editor: Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, model, cx);
|
||||
self.count_tokens(prompt_id, model, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
body_editor.update(cx, |body_editor, cx| {
|
||||
body_editor.change_selections(None, cx, |selections| {
|
||||
body_editor.update(cx, |body_editor, model, cx| {
|
||||
body_editor.change_selections(None, model, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -777,7 +855,7 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
@@ -785,7 +863,7 @@ impl PromptLibrary {
|
||||
let editor = &prompt.body_editor.read(cx);
|
||||
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
let body = buffer.as_rope().clone();
|
||||
prompt.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
prompt.pending_token_count = model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
@@ -808,10 +886,10 @@ impl PromptLibrary {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
.log_err()
|
||||
@@ -819,7 +897,7 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_prompt_list(&mut self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
@@ -839,7 +917,9 @@ impl PromptLibrary {
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(NewPrompt));
|
||||
}),
|
||||
@@ -848,7 +928,11 @@ impl PromptLibrary {
|
||||
.child(div().flex_grow().child(self.picker.clone()))
|
||||
}
|
||||
|
||||
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
|
||||
fn render_active_prompt(
|
||||
&mut self,
|
||||
model: &Model<PromptLibrary>,
|
||||
cx: &mut AppContext,
|
||||
) -> gpui::Stateful<Div> {
|
||||
div()
|
||||
.w_2_3()
|
||||
.h_full()
|
||||
@@ -873,7 +957,7 @@ impl PromptLibrary {
|
||||
.overflow_hidden()
|
||||
.pl(DynamicSpacing::Base16.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
@@ -927,7 +1011,7 @@ impl PromptLibrary {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
inlay_hints_style:
|
||||
editor::make_inlay_hints_style(cx),
|
||||
editor::make_inlay_hints_style(model, cx),
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
@@ -959,7 +1043,7 @@ impl PromptLibrary {
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
let token_count =
|
||||
token_count.clone();
|
||||
|
||||
@@ -978,6 +1062,7 @@ impl PromptLibrary {
|
||||
.0)
|
||||
.unwrap_or_default()
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -997,11 +1082,12 @@ impl PromptLibrary {
|
||||
Icon::new(IconName::FileLock)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1015,10 +1101,11 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1036,10 +1123,11 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Duplicate Prompt",
|
||||
&DuplicatePrompt,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1064,7 +1152,7 @@ impl PromptLibrary {
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
@@ -1098,8 +1186,13 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let theme = cx.theme().clone();
|
||||
|
||||
h_flex()
|
||||
@@ -1115,7 +1208,7 @@ impl Render for PromptLibrary {
|
||||
.overflow_hidden()
|
||||
.font(ui_font)
|
||||
.text_color(theme.colors().text)
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(self.render_prompt_list(model, cx))
|
||||
.map(|el| {
|
||||
if self.store.prompt_count() == 0 {
|
||||
el.child(
|
||||
@@ -1151,7 +1244,7 @@ impl Render for PromptLibrary {
|
||||
Button::new("create-prompt", "New Prompt")
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&NewPrompt, cx,
|
||||
&NewPrompt, window, cx,
|
||||
))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone())
|
||||
@@ -1162,7 +1255,7 @@ impl Render for PromptLibrary {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
el.child(self.render_active_prompt(cx))
|
||||
el.child(self.render_active_prompt(model, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::AfterCompletion;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use gpui::{AppContext, AppContext, Model, Task, WeakView};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::CompletionIntent;
|
||||
@@ -41,8 +41,8 @@ pub mod terminal_command;
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -55,8 +55,8 @@ pub(crate) struct SlashCommandLine {
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -71,7 +71,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_name: &str,
|
||||
command_range: Range<Anchor>,
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
@@ -120,12 +121,12 @@ impl SlashCommandCompletionProvider {
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
.update(cx, |editor, model, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
@@ -165,7 +166,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_range: Range<Anchor>,
|
||||
argument_range: Range<Anchor>,
|
||||
last_argument_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
@@ -203,12 +205,12 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
if new_argument.after_completion.run()
|
||||
|| intent.is_complete()
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
.update(cx, |editor, model, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
@@ -260,10 +262,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
model: &Model<Editor>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
buffer.update(cx, |buffer, model, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
@@ -318,7 +321,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.complete_command_name(&name, command_range, last_argument_range, cx)
|
||||
self.complete_command_name(&name, command_range, last_argument_range, model, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +330,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
_: &Model<Editor>,
|
||||
_: &mut AppContext,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
@@ -337,7 +341,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
_: &Model<Editor>,
|
||||
_: &mut AppContext,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
@@ -348,7 +353,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
model: &Model<Editor>,
|
||||
cx: &mut AppContext,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
use semantic_index::{FileSummary, SemanticDb};
|
||||
use smol::channel;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||
use ui::{prelude::*, BorrowAppContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
// There's no autocomplete for a prompt, since it's arbitrary text.
|
||||
// However, we can use this opportunity to kick off a drain of the backlog.
|
||||
@@ -76,7 +77,7 @@ impl SlashCommand for AutoCommand {
|
||||
|
||||
let cx: &mut AppContext = cx;
|
||||
|
||||
cx.spawn(|cx: gpui::AsyncAppContext| async move {
|
||||
cx.spawn(|cx: AsyncAppContext| async move {
|
||||
let task = project_index.read_with(&cx, |project_index, cx| {
|
||||
project_index.flush_summary_backlogs(cx)
|
||||
})?;
|
||||
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
@@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand {
|
||||
return Task::ready(Err(anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
|
||||
let task = cx.spawn(|cx: AsyncAppContext| async move {
|
||||
let summaries = project_index
|
||||
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
|
||||
.await?;
|
||||
|
||||
@@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -122,11 +123,12 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let output = workspace.update(cx, |workspace, model, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let fs = workspace.project().read(cx).fs().clone();
|
||||
let path = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
@@ -8,7 +8,7 @@ use context_server::{
|
||||
manager::{ContextServer, ContextServerManager},
|
||||
types::Prompt,
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
@@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
|
||||
return Task::ready(Err(anyhow!("Failed to complete argument")));
|
||||
@@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
@@ -164,7 +166,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| match msg.content {
|
||||
context_server::types::MessageContent::Text { text } => Some(text),
|
||||
context_server::types::MessageContent::Text { text, .. } => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
@@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -47,9 +48,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use text::OffsetRangeExt;
|
||||
@@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
_arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut paths = HashSet::default();
|
||||
let mut file_command_old_outputs = Vec::new();
|
||||
@@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
context_buffer.clone(),
|
||||
workspace.clone(),
|
||||
delegate.clone(),
|
||||
model,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl DiagnosticsSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -118,8 +118,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -172,9 +172,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -43,7 +43,7 @@ impl DocsSlashCommand {
|
||||
/// access the workspace so we can read the project.
|
||||
fn ensure_rust_doc_providers_are_registered(
|
||||
&self,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
@@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
@@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing an argument")));
|
||||
|
||||
@@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -135,9 +136,9 @@ impl SlashCommand for FetchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
|
||||
@@ -28,7 +28,7 @@ impl FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -134,8 +134,8 @@ impl SlashCommand for FileSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -187,9 +187,9 @@ impl SlashCommand for FileSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::PromptBuilder;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{Anchor, CodeLabel, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelTool};
|
||||
use schemars::JsonSchema;
|
||||
@@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
|
||||
context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let current_model = model_registry.active_model();
|
||||
|
||||
@@ -37,8 +37,8 @@ impl SlashCommand for PromptSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
@@ -64,9 +64,9 @@ impl SlashCommand for PromptSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let title = arguments.to_owned().join(" ");
|
||||
if title.trim().is_empty() {
|
||||
|
||||
@@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -69,9 +70,9 @@ impl SlashCommand for SearchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{AppContext, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use ui::{IconName, SharedString, WindowContext};
|
||||
use ui::{IconName, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct SelectionCommand;
|
||||
@@ -47,8 +47,9 @@ impl SlashCommand for SelectionCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -58,9 +59,10 @@ impl SlashCommand for SelectionCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut events = vec![];
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
cx.background_executor()
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::IconName;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OutlineSlashCommand;
|
||||
@@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -49,11 +50,12 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let output = workspace.update(cx, |workspace, model, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return Task::ready(Err(anyhow!("no active tab")));
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||
use ui::{prelude::*, ActiveTheme};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let mut has_all_tabs_completion_item = false;
|
||||
let argument_set = arguments
|
||||
@@ -73,8 +74,8 @@ impl SlashCommand for TabSlashCommand {
|
||||
|
||||
let active_item_path = workspace.as_ref().and_then(|workspace| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let snapshot = active_item_buffer(workspace, cx).ok()?;
|
||||
.update(cx, |workspace, model, cx| {
|
||||
let snapshot = active_item_buffer(workspace, model, cx).ok()?;
|
||||
snapshot.resolve_file_path(cx, true)
|
||||
})
|
||||
.ok()
|
||||
@@ -82,7 +83,7 @@ impl SlashCommand for TabSlashCommand {
|
||||
});
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, model, cx);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.spawn(|_| async move {
|
||||
@@ -137,9 +138,10 @@ impl SlashCommand for TabSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace),
|
||||
@@ -160,11 +162,12 @@ impl SlashCommand for TabSlashCommand {
|
||||
}
|
||||
|
||||
fn tab_items_for_queries(
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
queries: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
strict_match: bool,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
@@ -174,7 +177,7 @@ fn tab_items_for_queries(
|
||||
.context("no workspace")?
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
if strict_match && empty_query {
|
||||
let snapshot = active_item_buffer(workspace, cx)?;
|
||||
let snapshot = active_item_buffer(workspace, model, cx)?;
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
|
||||
}
|
||||
@@ -285,7 +288,8 @@ fn tab_items_for_queries(
|
||||
|
||||
fn active_item_buffer(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ui::ViewContext<Workspace>,
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> anyhow::Result<BufferSnapshot> {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -64,15 +65,16 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
|
||||
let Some(active_terminal) = resolve_active_terminal(&workspace, model, cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
@@ -107,9 +109,10 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
}
|
||||
|
||||
fn resolve_active_terminal(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &WindowContext,
|
||||
) -> Option<View<TerminalView>> {
|
||||
workspace: &Model<Workspace>,
|
||||
window: &Window,
|
||||
cx: &AppContext,
|
||||
) -> Option<Model<TerminalView>> {
|
||||
if let Some(terminal_view) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use crate::SlashCommandWorkingSet;
|
||||
@@ -10,7 +10,7 @@ use crate::SlashCommandWorkingSet;
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
renderer: fn(&mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
|
||||
on_confirm: fn(&mut gpui::Window, &mut gpui::AppContext),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ impl AsRef<str> for SlashCommandEntry {
|
||||
pub(crate) struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandEntry>,
|
||||
filtered_commands: Vec<SlashCommandEntry>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> SlashCommandSelector<T> {
|
||||
pub(crate) fn new(
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
) -> Self {
|
||||
SlashCommandSelector {
|
||||
@@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Select a command...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_commands.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -104,10 +109,10 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.filtered_commands = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -139,25 +144,31 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
model: &Model<Picker>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
.update(cx, |context_editor, model, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(cx);
|
||||
on_confirm(window, cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
@@ -167,7 +178,8 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let command_info = self.filtered_commands.get(ix)?;
|
||||
|
||||
@@ -177,11 +189,20 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = info.description.clone();
|
||||
move |cx| {
|
||||
cx.new_model(|_, _| Tooltip::new(description.clone()))
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.py_0p5()
|
||||
.min_w(px(250.))
|
||||
.max_w(px(400.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
@@ -192,7 +213,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
Label::new(label).single_line().size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
@@ -200,6 +221,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.single_line()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
@@ -210,9 +232,11 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -221,14 +245,14 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.child(renderer(cx)),
|
||||
.child(renderer(window, cx)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let all_models = self
|
||||
.working_set
|
||||
.featured_command_names(cx)
|
||||
@@ -296,14 +320,15 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
|
||||
let picker_view = cx.new_model(|model, cx| {
|
||||
let picker =
|
||||
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()));
|
||||
picker
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.update(cx, |this, model, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
@@ -23,17 +24,24 @@ editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
settings.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
258
crates/assistant2/src/active_thread.rs
Normal file
258
crates/assistant2/src/active_thread.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
|
||||
Subscription, TextStyleRefinement, View, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::Role;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread: Model<Thread>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Model<Thread>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| model.notify(cx)),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
language_registry,
|
||||
tools,
|
||||
thread: thread.clone(),
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = model.downgrade();
|
||||
move |ix, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
this.update(cx, |this, model, cx| this.render_message(ix, model, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
last_error: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, message.text.clone(), model, cx);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.messages.is_empty()
|
||||
}
|
||||
|
||||
pub fn summary(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
self.thread.read(cx).summary()
|
||||
}
|
||||
|
||||
pub fn last_error(&self) -> Option<ThreadError> {
|
||||
self.last_error.clone()
|
||||
}
|
||||
|
||||
pub fn clear_last_error(&mut self) {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
text: String,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let old_len = self.messages.len();
|
||||
self.messages.push(*id);
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = theme_settings.buffer_font_size;
|
||||
|
||||
let mut text_style = window.text_style();
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
color: Some(cx.theme().colors().text),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block: StyleRefinement {
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let markdown = cx.new_model(|model, cx| {
|
||||
Markdown::new(
|
||||
text,
|
||||
markdown_style,
|
||||
Some(self.language_registry.clone()),
|
||||
None,
|
||||
model,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
_: Model<Thread>,
|
||||
event: &ThreadEvent,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::StreamedCompletion => {}
|
||||
ThreadEvent::SummaryChanged => {}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
markdown.update(cx, |markdown, model, cx| {
|
||||
markdown.append(text, model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
ThreadEvent::MessageAdded(message_id) => {
|
||||
if let Some(message_text) = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(*message_id)
|
||||
.map(|message| message.text.clone())
|
||||
{
|
||||
self.push_message(message_id, message_text, model, cx);
|
||||
}
|
||||
|
||||
model.notify(cx);
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), model, cx);
|
||||
|
||||
self.thread.update(cx, |thread, model, cx| {
|
||||
thread.insert_tool_output(
|
||||
tool_use.assistant_message_id,
|
||||
tool_use.id.clone(),
|
||||
task,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, model: &Model<Self>, cx: &mut AppContext) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
Role::System => (IconName::Settings, "System"),
|
||||
};
|
||||
|
||||
div()
|
||||
.id(("message-container", ix))
|
||||
.p_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.p_1p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(role_icon).size(IconSize::Small))
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
list(self.list_state.clone()).flex_1()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod context_picker;
|
||||
mod message_editor;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -11,7 +14,13 @@ pub use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
[ToggleFocus, NewThread, ToggleModelSelector, Chat]
|
||||
[
|
||||
ToggleFocus,
|
||||
NewThread,
|
||||
ToggleModelSelector,
|
||||
OpenHistory,
|
||||
Chat
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
@@ -4,25 +4,28 @@ use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
prelude::*, px, svg, Action, AnyElement, AppContext, AppContext, EventEmitter, FocusHandle,
|
||||
FocusableView, FontWeight, Model, Pixels, Task, View, WeakView,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, Role};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{Message, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread::{ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
|workspace: &mut Workspace, model: &Model<Workspace>, _cx: &mut AppContext| {
|
||||
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
},
|
||||
@@ -30,22 +33,29 @@ pub fn init(cx: &mut AppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
enum ActiveView {
|
||||
Thread,
|
||||
History,
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
#[allow(unused)]
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
message_editor: View<MessageEditor>,
|
||||
thread: Model<ActiveThread>,
|
||||
message_editor: Model<MessageEditor>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history: Model<ThreadHistory>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
workspace: WeakModel<Workspace>,
|
||||
window: AnyWindowHandle,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store = workspace
|
||||
@@ -56,7 +66,7 @@ impl AssistantPanel {
|
||||
.await?;
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
|
||||
cx.new_model(|model, cx| Self::new(workspace, thread_store, tools, model, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -65,84 +75,110 @@ impl AssistantPanel {
|
||||
workspace: &Workspace,
|
||||
thread_store: Model<ThreadStore>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx));
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
let thread = thread_store.update(cx, |this, model, cx| this.create_thread(model, cx));
|
||||
let language_registry = workspace.project().read(cx).languages().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let weak_self = model.downgrade();
|
||||
|
||||
Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
workspace,
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_model(|model, cx| MessageEditor::new(thread.clone(), model, cx)),
|
||||
tools,
|
||||
last_error: None,
|
||||
_subscriptions: subscriptions,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
history: cx
|
||||
.new_model(|model, cx| ThreadHistory::new(weak_self, thread_store, model, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let tools = self.thread.read(cx).tools().clone();
|
||||
let thread = cx.new_model(|cx| Thread::new(tools, cx));
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
|
||||
self.thread = thread;
|
||||
self._subscriptions = subscriptions;
|
||||
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
pub(crate) fn local_timezone(&self) -> UtcOffset {
|
||||
self.local_timezone
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
fn new_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let thread = self
|
||||
.thread_store
|
||||
.update(cx, |this, model, cx| this.create_thread(model, cx));
|
||||
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
_: Model<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
thread_id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::StreamedCompletion => {}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
.update(cx, |this, model, cx| this.open_thread(thread_id, model, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(
|
||||
tool_use.assistant_message_id,
|
||||
tool_use.id.clone(),
|
||||
task,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished { .. } => {}
|
||||
}
|
||||
pub(crate) fn delete_thread(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.thread_store.update(cx, |this, model, cx| {
|
||||
this.delete_thread(thread_id, model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.message_editor.focus_handle(cx)
|
||||
match self.active_view {
|
||||
ActiveView::Thread => self.message_editor.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +189,7 @@ impl Panel for AssistantPanel {
|
||||
"AssistantPanel2"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
fn position(&self, _window: &Window, cx: &AppContext) -> DockPosition {
|
||||
DockPosition::Right
|
||||
}
|
||||
|
||||
@@ -161,25 +197,26 @@ impl Panel for AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_position(&mut self, _position: DockPosition, model: &Model<Self>, _cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
fn size(&self, _window: &Window, cx: &AppContext) -> Pixels {
|
||||
px(640.)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_size(&mut self, _size: Option<Pixels>, model: &Model<Self>, _cx: &mut AppContext) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, model: &Model<Self>, _cx: &mut AppContext) {}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::AssistantPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
fn icon(&self, _window: &Window, cx: &AppContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
|
||||
Some("Assistant Panel")
|
||||
}
|
||||
|
||||
@@ -189,7 +226,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_toolbar(&self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
@@ -201,11 +238,11 @@ impl AssistantPanel {
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().child(Label::new("Thread Title Goes Here")))
|
||||
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(self.render_language_model_selector(model, cx))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
@@ -219,12 +256,13 @@ impl AssistantPanel {
|
||||
"New Thread",
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("New Thread");
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(NewThread.boxed_clone());
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -232,9 +270,20 @@ impl AssistantPanel {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Open History", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("Open History");
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Open History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(OpenHistory.boxed_clone());
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -242,7 +291,7 @@ impl AssistantPanel {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
.tooltip(move |window, cx| Tooltip::text("Configure Assistant", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("Configure Assistant");
|
||||
}),
|
||||
@@ -250,7 +299,11 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_language_model_selector(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
@@ -297,39 +350,123 @@ impl AssistantPanel {
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, model, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
Role::System => (IconName::Settings, "System"),
|
||||
};
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
if self.thread.read(cx).is_empty() {
|
||||
return self.render_thread_empty_state(model, cx).into_any_element();
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.p_1p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(role_icon).size(IconSize::Small))
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().child(Label::new(message.text.clone())))
|
||||
self.thread.clone().into_any()
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
fn render_thread_empty_state(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let recent_threads = self
|
||||
.thread_store
|
||||
.update(cx, |this, model, cx| this.recent_threads(3, model, cx));
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.mx_auto()
|
||||
.child(
|
||||
v_flex().w_full().child(
|
||||
svg()
|
||||
.path("icons/logo_96.svg")
|
||||
.text_color(cx.theme().colors().text)
|
||||
.w(px(40.))
|
||||
.h(px(40.))
|
||||
.mx_auto()
|
||||
.mb_4(),
|
||||
),
|
||||
)
|
||||
.child(v_flex())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Context Examples:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Terminal)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("Terminal").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Folder)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("/src/components").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
v_flex().gap_2().children(
|
||||
recent_threads
|
||||
.into_iter()
|
||||
.map(|thread| PastThread::new(thread, model.downgrade())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Button::new("view-all-past-threads", "View All Past Threads")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenHistory,
|
||||
&self.focus_handle(cx),
|
||||
model,
|
||||
cx,
|
||||
))
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(OpenHistory.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_last_error(&self, model: &Model<Self>, cx: &mut AppContext) -> Option<AnyElement> {
|
||||
let last_error = self.thread.read(cx).last_error()?;
|
||||
|
||||
Some(
|
||||
div()
|
||||
@@ -342,19 +479,23 @@ impl AssistantPanel {
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(model, cx),
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
self.render_max_monthly_spend_reached_error(model, cx)
|
||||
}
|
||||
ThreadError::Message(error_message) => {
|
||||
self.render_error_message(error_message, cx)
|
||||
self.render_error_message(&error_message, model, cx)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_payment_required_error(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
|
||||
v_flex()
|
||||
@@ -379,22 +520,32 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_max_monthly_spend_reached_error(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
|
||||
v_flex()
|
||||
@@ -419,17 +570,23 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
this.last_error = None;
|
||||
model.listener(|this, model, _, cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
@@ -439,7 +596,8 @@ impl AssistantPanel {
|
||||
fn render_error_message(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
@@ -466,8 +624,11 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
@@ -476,9 +637,12 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.justify_between()
|
||||
@@ -486,27 +650,23 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(|this, _: &NewThread, cx| {
|
||||
this.new_thread(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.id("message-list")
|
||||
.gap_2()
|
||||
.size_full()
|
||||
.p_2()
|
||||
.overflow_y_scroll()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.children(
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|message| self.render_message(message, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(cx))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(window);
|
||||
model.notify(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(model, cx))
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(model, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(model, cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
207
crates/assistant2/src/context_picker.rs
Normal file
207
crates/assistant2/src/context_picker.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct ContextPicker<T: PopoverTrigger> {
|
||||
message_editor: WeakModel<MessageEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
all_entries: Vec<ContextPickerEntry>,
|
||||
filtered_entries: Vec<ContextPickerEntry>,
|
||||
message_editor: WeakModel<MessageEditor>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ContextPicker<T> {
|
||||
pub(crate) fn new(message_editor: WeakModel<MessageEditor>, trigger: T) -> Self {
|
||||
ContextPicker {
|
||||
message_editor,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_ix
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Select a context source…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_entries.clone();
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_commands
|
||||
} else {
|
||||
all_commands
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.filtered_entries = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
|
||||
self.message_editor
|
||||
.update(cx, |_message_editor, model, _cx| {
|
||||
println!("Insert context from {}", entry.name);
|
||||
})
|
||||
.ok();
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
model: &Model<Picker>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = self.filtered_entries.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = entry.description.clone();
|
||||
move |cx| {
|
||||
cx.new_model(|_model, _cx| Tooltip::new(description.clone()))
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.group(format!("context-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.py_0p5()
|
||||
.min_w(px(250.))
|
||||
.max_w(px(400.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(entry.icon).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(entry.name.clone())
|
||||
.single_line()
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(entry.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "web".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
all_entries: entries.clone(),
|
||||
message_editor: self.message_editor.clone(),
|
||||
filtered_entries: entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_model(|model, cx| {
|
||||
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.message_editor
|
||||
.update(cx, |this, model, _| this.context_picker_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(picker.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,70 @@
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use picker::Picker;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenuHandle,
|
||||
};
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
editor: Model<Editor>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(thread: Model<Thread>, model: &Model<Self>, cx: &mut AppContext) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything…", cx);
|
||||
editor: cx.new_model(|model, cx| {
|
||||
let mut editor = Editor::auto_height(80, model, cx);
|
||||
editor.set_placeholder_text("Ask anything…", model, cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
use_tools: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
fn chat(&mut self, _: &Chat, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.send_to_model(RequestKind::Chat, model, cx);
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry.active_model()?;
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let user_message = self.editor.update(cx, |editor, model, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message);
|
||||
self.thread.update(cx, |thread, model, cx| {
|
||||
thread.insert_user_message(user_message, cx);
|
||||
let mut request = thread.to_completion_request(request_kind, cx);
|
||||
|
||||
if self.use_tools {
|
||||
@@ -86,7 +94,12 @@ impl FocusableView for MessageEditor {
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
@@ -98,6 +111,14 @@ impl Render for MessageEditor {
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex().gap_2().child(ContextPicker::new(
|
||||
model.downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)),
|
||||
)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
@@ -123,26 +144,17 @@ impl Render for MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Button::new("add-context", "Add Context")
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start),
|
||||
)
|
||||
.child(CheckboxWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(h_flex().gap_2().child(CheckboxWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -154,11 +166,11 @@ impl Render for MessageEditor {
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Chat"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, model, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
focus_handle.dispatch_action(&Chat, model, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,24 +2,41 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
|
||||
use gpui::{AppContext, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
StopReason,
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
};
|
||||
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ThreadId(Arc<str>);
|
||||
|
||||
impl ThreadId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ThreadId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MessageId(usize);
|
||||
|
||||
@@ -39,6 +56,10 @@ pub struct Message {
|
||||
|
||||
/// A thread of conversation with the LLM.
|
||||
pub struct Thread {
|
||||
id: ThreadId,
|
||||
updated_at: DateTime<Utc>,
|
||||
summary: Option<SharedString>,
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
completion_count: usize,
|
||||
@@ -50,8 +71,12 @@ pub struct Thread {
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, model: &Model<Self>, _cx: &mut AppContext) -> Self {
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
completion_count: 0,
|
||||
@@ -63,6 +88,40 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ThreadId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.messages.is_empty()
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> DateTime<Utc> {
|
||||
self.updated_at
|
||||
}
|
||||
|
||||
pub fn touch_updated_at(&mut self) {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
|
||||
pub fn set_summary(
|
||||
&mut self,
|
||||
summary: impl Into<SharedString>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.summary = Some(summary.into());
|
||||
model.emit(ThreadEvent::SummaryChanged, cx);
|
||||
}
|
||||
|
||||
pub fn message(&self, id: MessageId) -> Option<&Message> {
|
||||
self.messages.iter().find(|message| message.id == id)
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> impl Iterator<Item = &Message> {
|
||||
self.messages.iter()
|
||||
}
|
||||
@@ -75,12 +134,30 @@ impl Thread {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn insert_user_message(&mut self, text: impl Into<String>) {
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.insert_message(Role::User, text, model, cx)
|
||||
}
|
||||
|
||||
pub fn insert_message(
|
||||
&mut self,
|
||||
role: Role,
|
||||
text: impl Into<String>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let id = self.next_message_id.post_inc();
|
||||
self.messages.push(Message {
|
||||
id: self.next_message_id.post_inc(),
|
||||
role: Role::User,
|
||||
id,
|
||||
role,
|
||||
text: text.into(),
|
||||
});
|
||||
self.touch_updated_at();
|
||||
model.emit(ThreadEvent::MessageAdded(id), cx);
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
@@ -134,7 +211,8 @@ impl Thread {
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
|
||||
@@ -150,11 +228,7 @@ impl Thread {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
match event {
|
||||
LanguageModelCompletionEvent::StartMessage { .. } => {
|
||||
thread.messages.push(Message {
|
||||
id: thread.next_message_id.post_inc(),
|
||||
role: Role::Assistant,
|
||||
text: String::new(),
|
||||
});
|
||||
thread.insert_message(Role::Assistant, String::new(), cx);
|
||||
}
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
@@ -163,6 +237,13 @@ impl Thread {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.text.push_str(&chunk);
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::StreamedAssistantText(
|
||||
last_message.id,
|
||||
chunk,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,17 +273,22 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::StreamedCompletion);
|
||||
cx.notify();
|
||||
thread.touch_updated_at();
|
||||
model.emit(ThreadEvent::StreamedCompletion, cx);
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
|
||||
thread.update(&mut cx, |thread, _cx| {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.pending_completions
|
||||
.retain(|completion| completion.id != pending_completion_id);
|
||||
|
||||
if thread.summary.is_none() && thread.messages.len() >= 2 {
|
||||
thread.summarize(cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(stop_reason)
|
||||
@@ -214,25 +300,31 @@ impl Thread {
|
||||
.update(&mut cx, |_thread, cx| match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
cx.emit(ThreadEvent::UsePendingTools);
|
||||
model.emit(ThreadEvent::UsePendingTools, cx);
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
},
|
||||
Err(error) => {
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
model.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired), cx);
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached),
|
||||
);
|
||||
} else {
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
|
||||
SharedString::from(error_message.clone()),
|
||||
)));
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::ShowError(ThreadError::Message(SharedString::from(
|
||||
error_message.clone(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -245,12 +337,66 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
|
||||
return;
|
||||
};
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !provider.is_authenticated(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut request = self.to_completion_request(RequestKind::Chat, cx);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
self.pending_summary = model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
|
||||
let mut new_summary = String::new();
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
let text = message?;
|
||||
let mut lines = text.lines();
|
||||
new_summary.extend(lines.next());
|
||||
|
||||
// Stop if the LLM generated multiple lines.
|
||||
if lines.next().is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if !new_summary.is_empty() {
|
||||
this.summary = Some(new_summary.into());
|
||||
}
|
||||
|
||||
model.emit(ThreadEvent::SummaryChanged, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let insert_output_task = cx.spawn(|thread, mut cx| {
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
@@ -276,7 +422,7 @@ impl Thread {
|
||||
is_error: false,
|
||||
});
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
|
||||
model.emit(ThreadEvent::ToolFinished { tool_use_id }, cx);
|
||||
}
|
||||
Err(err) => {
|
||||
tool_results.push(LanguageModelToolResult {
|
||||
@@ -316,6 +462,9 @@ pub enum ThreadError {
|
||||
pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
MessageAdded(MessageId),
|
||||
SummaryChanged,
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
#[allow(unused)]
|
||||
|
||||
164
crates/assistant2/src/thread_history.rs
Normal file
164
crates/assistant2/src/thread_history.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use gpui::{
|
||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem};
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::AssistantPanel;
|
||||
|
||||
pub struct ThreadHistory {
|
||||
focus_handle: FocusHandle,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle: window.focus_handle(),
|
||||
assistant_panel,
|
||||
thread_store,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ThreadHistory {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let threads = self
|
||||
.thread_store
|
||||
.update(cx, |this, model, cx| this.threads(cx));
|
||||
|
||||
v_flex()
|
||||
.id("thread-history-container")
|
||||
.track_focus(&self.focus_handle)
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.p_1()
|
||||
.map(|history| {
|
||||
if threads.is_empty() {
|
||||
history
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
history.child(
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
"thread-history",
|
||||
threads.len(),
|
||||
move |history, range, _cx| {
|
||||
threads[range]
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: Model<Thread>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
assistant_panel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let (id, summary) = {
|
||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
let thread = self.thread.read(cx);
|
||||
(
|
||||
thread.id().clone(),
|
||||
thread.summary().unwrap_or(DEFAULT_SUMMARY),
|
||||
)
|
||||
};
|
||||
|
||||
let thread_timestamp = time_format::format_localized_timestamp(
|
||||
OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
|
||||
.unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, model, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
ListItem::new(("past-thread", self.thread.entity_id()))
|
||||
.start_slot(Icon::new(IconName::MessageBubbles))
|
||||
.child(Label::new(summary))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(thread_timestamp).color(Color::Disabled))
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, model, cx| {
|
||||
this.delete_thread(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, model, cx| {
|
||||
this.open_thread(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,20 @@ use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
|
||||
use gpui::{prelude::*, AppContext, Model, Task};
|
||||
use project::Project;
|
||||
use unindent::Unindent;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
|
||||
pub struct ThreadStore {
|
||||
#[allow(unused)]
|
||||
project: Model<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<Model<Thread>>,
|
||||
}
|
||||
|
||||
impl ThreadStore {
|
||||
@@ -24,19 +28,26 @@ impl ThreadStore {
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new_model(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
let context_server_manager = cx.new_model(|model, cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
let mut this = Self {
|
||||
project,
|
||||
tools,
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
};
|
||||
this.mock_recent_threads(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
|
||||
this
|
||||
@@ -46,7 +57,49 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
|
||||
pub fn threads(&self, model: &Model<Self>, cx: &AppContext) -> Vec<Model<Thread>> {
|
||||
let mut threads = self
|
||||
.threads
|
||||
.iter()
|
||||
.filter(|thread| !thread.read(cx).is_empty())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at()));
|
||||
threads
|
||||
}
|
||||
|
||||
pub fn recent_threads(
|
||||
&self,
|
||||
limit: usize,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<Model<Thread>> {
|
||||
self.threads(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Thread> {
|
||||
let thread = cx.new_model(|model, cx| Thread::new(self.tools.clone(), model, cx));
|
||||
self.threads.push(thread.clone());
|
||||
thread
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
&self,
|
||||
id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Model<Thread>> {
|
||||
self.threads
|
||||
.iter()
|
||||
.find(|thread| thread.read(cx).id() == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn delete_thread(&mut self, id: &ThreadId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.threads.retain(|thread| thread.read(cx).id() != id);
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
Self::handle_context_server_event,
|
||||
@@ -58,7 +111,8 @@ impl ThreadStore {
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let tool_working_set = self.tools.clone();
|
||||
match event {
|
||||
@@ -112,3 +166,93 @@ impl ThreadStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadStore {
|
||||
/// Creates some mocked recent threads for testing purposes.
|
||||
fn mock_recent_threads(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
use language_model::Role;
|
||||
|
||||
self.threads.push(cx.new_model(|model, cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), model, cx);
|
||||
thread.set_summary("Introduction to quantum computing", model, cx);
|
||||
thread.insert_user_message("Hello! Can you help me understand quantum computing?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", model, cx);
|
||||
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", model, cx);
|
||||
thread
|
||||
}));
|
||||
|
||||
self.threads.push(cx.new_model(|model, cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), model, cx);
|
||||
thread.set_summary("Rust web development and async programming", model, cx);
|
||||
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
|
||||
|
||||
```rust
|
||||
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
|
||||
|
||||
async fn hello() -> impl Responder {
|
||||
HttpResponse::Ok().body(\"Hello, World!\")
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.route(\"/\", web::get().to(hello))
|
||||
})
|
||||
.bind(\"127.0.0.1:8080\")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening:
|
||||
|
||||
1. We import necessary items from the `actix-web` crate.
|
||||
2. We define an async `hello` function that returns a simple HTTP response.
|
||||
3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`.
|
||||
4. We configure the app to respond to GET requests on the root path with our `hello` function.
|
||||
|
||||
To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = \"4.0\"
|
||||
```
|
||||
|
||||
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), model, cx);
|
||||
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
|
||||
|
||||
1. **Syntax**: Async functions are declared using the `async` keyword:
|
||||
|
||||
```rust
|
||||
async fn my_async_function() -> Result<(), Error> {
|
||||
// Asynchronous code here
|
||||
}
|
||||
```
|
||||
|
||||
2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point.
|
||||
|
||||
3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete:
|
||||
|
||||
```rust
|
||||
async fn fetch_data() -> Result<String, Error> {
|
||||
let response = make_http_request().await?;
|
||||
let data = process_response(response).await?;
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources.
|
||||
|
||||
5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime.
|
||||
|
||||
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
|
||||
|
||||
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), model, cx);
|
||||
thread
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -78,8 +78,9 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
@@ -90,21 +91,27 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||
// what the extension API is already expecting.
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult>;
|
||||
}
|
||||
|
||||
pub type RenderFoldPlaceholder = Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
+ Fn(
|
||||
ElementId,
|
||||
Arc<dyn Fn(&mut gpui::Window, &mut gpui::AppContext)>,
|
||||
&mut gpui::Window,
|
||||
&mut gpui::AppContext,
|
||||
) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
@@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
|
||||
@@ -4,7 +4,7 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: WeakModel<Workspace>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{Task, WeakView};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -41,8 +41,9 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakView<workspace::Workspace>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: WeakModel<workspace::Workspace>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<String>> {
|
||||
let input: FileToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
|
||||
@@ -3,8 +3,7 @@ use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, SemanticVersion, Task,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use paths::remote_servers_dir;
|
||||
@@ -131,16 +130,16 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
workspace.register_action(model, |_, action: &Check, cx| check(action, model, cx));
|
||||
|
||||
workspace.register_action(|_, action, cx| {
|
||||
workspace.register_action(model, |_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let auto_updater = cx.new_model(|model, cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let poll_for_updates = ReleaseChannel::try_global(cx)
|
||||
@@ -153,7 +152,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
.then(|| updater.start_polling(model, cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
@@ -172,7 +171,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
pub fn check(_: &Check, window: &mut gpui::Window, cx: &mut gpui::AppContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
@@ -201,7 +200,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
updater.update(cx, |updater, model, cx| updater.poll(model, cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
@@ -249,30 +248,30 @@ impl AutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
pub fn start_polling(&self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
loop {
|
||||
this.update(&mut cx, |this, cx| this.poll(cx))?;
|
||||
this.update(&mut cx, |this, model, cx| this.poll(model, cx))?;
|
||||
cx.background_executor().timer(POLL_INTERVAL).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn poll(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if self.pending_poll.is_some() || self.status.is_updated() {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
|
||||
self.pending_poll = Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let result = Self::update(this.upgrade()?, cx.clone()).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_poll = None;
|
||||
if let Err(error) = result {
|
||||
log::error!("auto-update failed: error:{:?}", error);
|
||||
this.status = AutoUpdateStatus::Errored;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
@@ -287,9 +286,9 @@ impl AutoUpdater {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn dismiss_error(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
|
||||
@@ -432,15 +431,16 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
let (client, current_version, release_channel) =
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
model.notify(cx);
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let release =
|
||||
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
|
||||
@@ -455,16 +455,16 @@ impl AutoUpdater {
|
||||
};
|
||||
|
||||
if !should_download {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Downloading;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
@@ -485,9 +485,9 @@ impl AutoUpdater {
|
||||
let downloaded_asset = temp_dir.path().join(filename);
|
||||
download_release(&downloaded_asset, release, client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
let binary_path = match OS {
|
||||
@@ -496,11 +496,11 @@ impl AutoUpdater {
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated { binary_path };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,7 @@ mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use gpui::{actions, prelude::*, AppContext, Model, SharedString, View};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
@@ -18,8 +18,8 @@ actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
workspace.register_action(model, |workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, model, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -31,7 +31,11 @@ struct ReleaseNotesBody {
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
fn view_release_notes_locally(
|
||||
workspace: &mut Workspace,
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
@@ -60,7 +64,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
.with_local_workspace(model, cx, move |_, model, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
@@ -78,27 +82,29 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let buffer = project.update(cx, |project, model, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer =
|
||||
cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
let editor = cx.new_model(|model, cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, model, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
let view: Model<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
@@ -107,7 +113,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -117,7 +123,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
pub fn notify_of_any_new_update(model: &Model<Workspace>, cx: &mut AppContext) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
@@ -130,9 +136,9 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
|cx| cx.new_model(|_, _| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.update(cx, |updater, model, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
|
||||
div, AppContext, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakView,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use release_channel::ReleaseChannel;
|
||||
@@ -12,13 +12,18 @@ use workspace::{
|
||||
|
||||
pub struct UpdateNotification {
|
||||
version: SemanticVersion,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let app_name = ReleaseChannel::global(cx).display_name();
|
||||
|
||||
v_flex()
|
||||
@@ -37,7 +42,10 @@ impl Render for UpdateNotification {
|
||||
.id("cancel")
|
||||
.child(Icon::new(IconName::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
.on_click(
|
||||
model
|
||||
.listener(|this, model, _, cx| this.dismiss(&menu::Cancel, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -45,10 +53,10 @@ impl Render for UpdateNotification {
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
.on_click(model.listener(|this, model, _, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(workspace, cx);
|
||||
.update(cx, |workspace, model, cx| {
|
||||
crate::view_release_notes_locally(workspace, model, cx);
|
||||
})
|
||||
.log_err();
|
||||
this.dismiss(&menu::Cancel, cx)
|
||||
@@ -58,11 +66,11 @@ impl Render for UpdateNotification {
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakView<Workspace>) -> Self {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakModel<Workspace>) -> Self {
|
||||
Self { version, workspace }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
pub fn dismiss(&mut self, _: &Cancel, model: &Model<Self>, cx: &mut AppContext) {
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
|
||||
Subscription, ViewContext,
|
||||
AppContext, Element, EventEmitter, FocusableView, IntoElement, Model, ParentElement, Render,
|
||||
StyledText, Subscription,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::cmp;
|
||||
@@ -37,7 +37,12 @@ impl Breadcrumbs {
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
const MAX_SEGMENTS: usize = 12;
|
||||
let element = h_flex().text_ui(cx);
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
@@ -64,7 +69,7 @@ impl Render for Breadcrumbs {
|
||||
}
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
let mut text_style = cx.text_style();
|
||||
let mut text_style = window.text_style();
|
||||
if let Some(font) = segment.font {
|
||||
text_style.font_family = font.family;
|
||||
text_style.font_features = font.features;
|
||||
@@ -94,23 +99,25 @@ impl Render for Breadcrumbs {
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, model, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -128,26 +135,30 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
self.active_item = None;
|
||||
|
||||
let Some(item) = active_pane_item else {
|
||||
return ToolbarItemLocation::Hidden;
|
||||
};
|
||||
|
||||
let this = cx.view().downgrade();
|
||||
let this = model.downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
model.emit(
|
||||
cx,
|
||||
ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -158,7 +169,7 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
item.breadcrumb_location(cx)
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &Model<Self>, _: &mut AppContext) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"live_kit_client/test-support",
|
||||
"livekit_client/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
@@ -27,11 +27,11 @@ anyhow.workspace = true
|
||||
audio.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
live_kit_client.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
@@ -41,13 +41,24 @@ serde_derive.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
livekit_client_macos = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
livekit_client = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
live_kit_client = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dev-dependencies]
|
||||
livekit_client_macos = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
|
||||
livekit_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,546 +1,13 @@
|
||||
pub mod call_settings;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
|
||||
Task, WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::*;
|
||||
|
||||
struct GlobalActiveCall(Model<ActiveCall>);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod cross_platform;
|
||||
|
||||
impl Global for GlobalActiveCall {}
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
CallSettings::register(cx);
|
||||
|
||||
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||
cx.set_global(GlobalActiveCall(active_call));
|
||||
}
|
||||
|
||||
pub struct OneAtATime {
|
||||
cancel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl OneAtATime {
|
||||
/// spawn a task in the given context.
|
||||
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
|
||||
/// otherwise you'll see the result of the task.
|
||||
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
|
||||
where
|
||||
F: 'static + FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: Future<Output = Result<R>>,
|
||||
R: 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.cancel.replace(tx);
|
||||
cx.spawn(|cx| async move {
|
||||
futures::select_biased! {
|
||||
_ = rx.fuse() => Ok(None),
|
||||
result = f(cx).fuse() => result.map(Some),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.cancel
|
||||
.as_ref()
|
||||
.is_some_and(|cancel| !cancel.is_canceled())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IncomingCall {
|
||||
pub room_id: u64,
|
||||
pub calling_user: Arc<User>,
|
||||
pub participants: Vec<Arc<User>>,
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(Model<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakModel<Project>>,
|
||||
_join_debouncer: OneAtATime,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
watch::Sender<Option<IncomingCall>>,
|
||||
watch::Receiver<Option<IncomingCall>>,
|
||||
),
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||
})?
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, cx)
|
||||
})?
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
})?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_call_canceled(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
.map_or(false, |call| call.room_id == envelope.payload.room_id)
|
||||
{
|
||||
incoming_call.take();
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<GlobalActiveCall>().0.clone()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
|
||||
cx.try_global::<GlobalActiveCall>()
|
||||
.map(|call| call.0.clone())
|
||||
}
|
||||
|
||||
pub fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, move |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(move |this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.background_executor().spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
cx.notify();
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no active call")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::CancelCall {
|
||||
room_id,
|
||||
called_user_id,
|
||||
})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
|
||||
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
|
||||
call
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no incoming call")));
|
||||
};
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room_id = call.room_id;
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self._join_debouncer.spawn(cx, move |cx| async move {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
cx.emit(Event::RoomLeft { channel_id });
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&WeakModel<Project>> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<Model<Room>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
cx.notify();
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&room, |this, room, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
.location
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
cx.emit(Event::RoomJoined { channel_id });
|
||||
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&Model<Room>> {
|
||||
self.room.as_ref().map(|(room, _)| room)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::OneAtATime;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_one_at_a_time(cx: &mut TestAppContext) {
|
||||
let mut one_at_a_time = OneAtATime { cancel: None };
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let (a, b) = cx.update(|cx| {
|
||||
(
|
||||
one_at_a_time.spawn(cx, |_| async {
|
||||
panic!("");
|
||||
}),
|
||||
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(a.await.unwrap(), None::<u32>);
|
||||
assert_eq!(b.await.unwrap(), Some(3));
|
||||
|
||||
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
|
||||
drop(one_at_a_time);
|
||||
|
||||
assert_eq!(promise.await.unwrap(), None);
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use cross_platform::*;
|
||||
|
||||
584
crates/call/src/cross_platform/mod.rs
Normal file
584
crates/call/src/cross_platform/mod.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use crate::call_settings::CallSettings;
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use livekit_client::{
|
||||
track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
|
||||
struct GlobalActiveCall(Model<ActiveCall>);
|
||||
|
||||
impl Global for GlobalActiveCall {}
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
livekit_client::init(
|
||||
cx.background_executor().dispatcher.clone(),
|
||||
cx.http_client(),
|
||||
);
|
||||
CallSettings::register(cx);
|
||||
|
||||
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
|
||||
cx.set_global(GlobalActiveCall(active_call));
|
||||
}
|
||||
|
||||
pub struct OneAtATime {
|
||||
cancel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl OneAtATime {
|
||||
/// spawn a task in the given context.
|
||||
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
|
||||
/// otherwise you'll see the result of the task.
|
||||
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
|
||||
where
|
||||
F: 'static + FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: Future<Output = Result<R>>,
|
||||
R: 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.cancel.replace(tx);
|
||||
cx.spawn(|cx| async move {
|
||||
futures::select_biased! {
|
||||
_ = rx.fuse() => Ok(None),
|
||||
result = f(cx).fuse() => result.map(Some),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.cancel
|
||||
.as_ref()
|
||||
.is_some_and(|cancel| !cancel.is_canceled())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IncomingCall {
|
||||
pub room_id: u64,
|
||||
pub calling_user: Arc<User>,
|
||||
pub participants: Vec<Arc<User>>,
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(Model<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakModel<Project>>,
|
||||
_join_debouncer: OneAtATime,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
watch::Sender<Option<IncomingCall>>,
|
||||
watch::Receiver<Option<IncomingCall>>,
|
||||
),
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
model: &Model<Self>,
|
||||
_: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
})?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_call_canceled(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
.map_or(false, |call| call.room_id == envelope.payload.room_id)
|
||||
{
|
||||
incoming_call.take();
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<GlobalActiveCall>().0.clone()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
|
||||
cx.try_global::<GlobalActiveCall>()
|
||||
.map(|call| call.0.clone())
|
||||
}
|
||||
|
||||
pub fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
model.notify(cx);
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, move |room, model, cx| {
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(Some(room.clone()), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.background_executor().spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, _model, cx| {
|
||||
this.report_call_event("invite", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
_: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no active call")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::CancelCall {
|
||||
room_id,
|
||||
called_user_id,
|
||||
})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
|
||||
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
|
||||
call
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no incoming call")));
|
||||
};
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room_id = call.room_id;
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, model, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self._join_debouncer.spawn(cx, move |cx| async move {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
model.emit(Event::RoomLeft { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| room.leave(model, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.unshare_project(project, model, cx)
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&WeakModel<Project>> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<Model<Room>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
model.notify(cx);
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
model.observe(&room, cx, |this, room, model, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, model, cx).detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
model.notify(cx);
|
||||
}),
|
||||
model.subscribe(&room, cx, |_, _, event, model, cx| {
|
||||
model.emit(event.clone(), cx)
|
||||
}),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
.location
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
model.emit(Event::RoomJoined { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.set_location(location.as_ref(), model, cx)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&Model<Room>> {
|
||||
self.room.as_ref().map(|(room, _)| room)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::OneAtATime;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_one_at_a_time(cx: &mut TestAppContext) {
|
||||
let mut one_at_a_time = OneAtATime { cancel: None };
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let (a, b) = cx.update(|cx| {
|
||||
(
|
||||
one_at_a_time.spawn(cx, |_| async {
|
||||
panic!("");
|
||||
}),
|
||||
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(a.await.unwrap(), None::<u32>);
|
||||
assert_eq!(b.await.unwrap(), Some(3));
|
||||
|
||||
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
|
||||
drop(one_at_a_time);
|
||||
|
||||
assert_eq!(promise.await.unwrap(), None);
|
||||
}
|
||||
}
|
||||
68
crates/call/src/cross_platform/participant.rs
Normal file
68
crates/call/src/cross_platform/participant.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
#![cfg_attr(target_os = "windows", allow(unused))]
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, ParticipantIndex, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
use livekit_client::AudioStream;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use livekit_client::id::TrackSid;
|
||||
pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ParticipantLocation {
|
||||
SharedProject { project_id: u64 },
|
||||
UnsharedProject,
|
||||
External,
|
||||
}
|
||||
|
||||
impl ParticipantLocation {
|
||||
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
|
||||
match location.and_then(|l| l.variant) {
|
||||
Some(proto::participant_location::Variant::SharedProject(project)) => {
|
||||
Ok(Self::SharedProject {
|
||||
project_id: project.id,
|
||||
})
|
||||
}
|
||||
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
|
||||
Ok(Self::UnsharedProject)
|
||||
}
|
||||
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
|
||||
None => Err(anyhow!("participant location was not provided")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LocalParticipant {
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub active_project: Option<WeakModel<Project>>,
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
|
||||
pub struct RemoteParticipant {
|
||||
pub user: Arc<User>,
|
||||
pub peer_id: proto::PeerId,
|
||||
pub role: proto::ChannelRole,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub location: ParticipantLocation,
|
||||
pub participant_index: ParticipantIndex,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
|
||||
}
|
||||
|
||||
impl RemoteParticipant {
|
||||
pub fn has_video_tracks(&self) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return !self.video_tracks.is_empty();
|
||||
#[cfg(target_os = "windows")]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1841
crates/call/src/cross_platform/room.rs
Normal file
1841
crates/call/src/cross_platform/room.rs
Normal file
File diff suppressed because it is too large
Load Diff
577
crates/call/src/macos/mod.rs
Normal file
577
crates/call/src/macos/mod.rs
Normal file
@@ -0,0 +1,577 @@
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use crate::call_settings::CallSettings;
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
|
||||
struct GlobalActiveCall(Model<ActiveCall>);
|
||||
|
||||
impl Global for GlobalActiveCall {}
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
CallSettings::register(cx);
|
||||
|
||||
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
|
||||
cx.set_global(GlobalActiveCall(active_call));
|
||||
}
|
||||
|
||||
pub struct OneAtATime {
|
||||
cancel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl OneAtATime {
|
||||
/// spawn a task in the given context.
|
||||
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
|
||||
/// otherwise you'll see the result of the task.
|
||||
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
|
||||
where
|
||||
F: 'static + FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: Future<Output = Result<R>>,
|
||||
R: 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.cancel.replace(tx);
|
||||
cx.spawn(|cx| async move {
|
||||
futures::select_biased! {
|
||||
_ = rx.fuse() => Ok(None),
|
||||
result = f(cx).fuse() => result.map(Some),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.cancel
|
||||
.as_ref()
|
||||
.is_some_and(|cancel| !cancel.is_canceled())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IncomingCall {
|
||||
pub room_id: u64,
|
||||
pub calling_user: Arc<User>,
|
||||
pub participants: Vec<Arc<User>>,
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(Model<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakModel<Project>>,
|
||||
_join_debouncer: OneAtATime,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
watch::Sender<Option<IncomingCall>>,
|
||||
watch::Receiver<Option<IncomingCall>>,
|
||||
),
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
})?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_call_canceled(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
.map_or(false, |call| call.room_id == envelope.payload.room_id)
|
||||
{
|
||||
incoming_call.take();
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<GlobalActiveCall>().0.clone()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
|
||||
cx.try_global::<GlobalActiveCall>()
|
||||
.map(|call| call.0.clone())
|
||||
}
|
||||
|
||||
pub fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
model.notify(cx);
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, move |room, model, cx| {
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(Some(room.clone()), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.background_executor().spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("invite", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no active call")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::CancelCall {
|
||||
room_id,
|
||||
called_user_id,
|
||||
})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
|
||||
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
|
||||
call
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no incoming call")));
|
||||
};
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room_id = call.room_id;
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, model, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self._join_debouncer.spawn(cx, move |cx| async move {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
model.emit(Event::RoomLeft { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| room.leave(model, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.unshare_project(project, model, cx)
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&WeakModel<Project>> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<Model<Room>>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
model.notify(cx);
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
model.observe(&room, cx, |this, room, model, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
model.notify(cx);
|
||||
}),
|
||||
model.subscribe(&room, cx, |_, _, event, model, cx| {
|
||||
model.emit(event.clone(), cx)
|
||||
}),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
.location
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
model.emit(Event::RoomJoined { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.set_location(location.as_ref(), model, cx)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&Model<Room>> {
|
||||
self.room.as_ref().map(|(room, _)| room)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::OneAtATime;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_one_at_a_time(cx: &mut TestAppContext) {
|
||||
let mut one_at_a_time = OneAtATime { cancel: None };
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let (a, b) = cx.update(|cx| {
|
||||
(
|
||||
one_at_a_time.spawn(cx, |_| async {
|
||||
panic!("");
|
||||
}),
|
||||
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(a.await.unwrap(), None::<u32>);
|
||||
assert_eq!(b.await.unwrap(), Some(3));
|
||||
|
||||
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
|
||||
drop(one_at_a_time);
|
||||
|
||||
assert_eq!(promise.await.unwrap(), None);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ use client::ParticipantIndex;
|
||||
use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
pub use live_kit_client::Frame;
|
||||
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
pub use livekit_client_macos::Frame;
|
||||
pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -49,6 +49,12 @@ pub struct RemoteParticipant {
|
||||
pub participant_index: ParticipantIndex,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
|
||||
pub video_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteVideoTrack>>,
|
||||
pub audio_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteAudioTrack>>,
|
||||
}
|
||||
|
||||
impl RemoteParticipant {
|
||||
pub fn has_video_tracks(&self) -> bool {
|
||||
!self.video_tracks.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,9 @@ use client::{
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
|
||||
use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use settings::Settings as _;
|
||||
@@ -97,7 +95,7 @@ impl Room {
|
||||
if let Some(live_kit) = self.live_kit.as_ref() {
|
||||
matches!(
|
||||
*live_kit.room.status().borrow(),
|
||||
live_kit_client::ConnectionState::Connected { .. }
|
||||
livekit_client_macos::ConnectionState::Connected { .. }
|
||||
)
|
||||
} else {
|
||||
false
|
||||
@@ -110,14 +108,15 @@ impl Room {
|
||||
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
|
||||
let room = live_kit_client::Room::new();
|
||||
let room = livekit_client_macos::Room::new();
|
||||
let mut status = room.status();
|
||||
// Consume the initial status of the room.
|
||||
let _ = status.try_recv();
|
||||
let _maintain_room = cx.spawn(|this, mut cx| async move {
|
||||
let _maintain_room = model.spawn(cx, |this, mut cx| async move {
|
||||
while let Some(status) = status.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
@@ -125,15 +124,15 @@ impl Room {
|
||||
break;
|
||||
};
|
||||
|
||||
if status == live_kit_client::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
|
||||
if status == livekit_client_macos::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, model, cx| this.leave(model, cx).log_err())
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _handle_updates = cx.spawn({
|
||||
let _handle_updates = model.spawn(cx, {
|
||||
let room = room.clone();
|
||||
move |this, mut cx| async move {
|
||||
let mut updates = room.updates();
|
||||
@@ -144,8 +143,8 @@ impl Room {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.live_kit_room_updated(update, cx).log_err()
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.live_kit_room_updated(update, model, cx).log_err()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -153,21 +152,22 @@ impl Room {
|
||||
});
|
||||
|
||||
let connect = room.connect(&connection_info.server_url, &connection_info.token);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.can_use_microphone() {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(cx);
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.can_use_microphone(cx) {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Task::ready(Ok(()))
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(LiveKitRoom {
|
||||
room,
|
||||
@@ -184,7 +184,7 @@ impl Room {
|
||||
None
|
||||
};
|
||||
|
||||
let maintain_connection = cx.spawn({
|
||||
let maintain_connection = model.spawn(cx, {
|
||||
let client = client.clone();
|
||||
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
|
||||
});
|
||||
@@ -206,11 +206,11 @@ impl Room {
|
||||
pending_participants: Default::default(),
|
||||
pending_call_count: 0,
|
||||
client_subscriptions: vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
|
||||
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
|
||||
],
|
||||
_subscriptions: vec![
|
||||
cx.on_release(Self::released),
|
||||
cx.on_app_quit(Self::app_will_quit),
|
||||
model.on_release(cx, Self::released),
|
||||
model.on_app_quit(cx, Self::app_will_quit),
|
||||
],
|
||||
leave_when_empty: false,
|
||||
pending_room_update: None,
|
||||
@@ -233,13 +233,14 @@ impl Room {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let response = client.request(proto::CreateRoom {}).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
let mut room = Self::new(
|
||||
room_proto.id,
|
||||
None,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
if let Some(participant) = room_proto.participants.first() {
|
||||
@@ -250,8 +251,8 @@ impl Room {
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
let initial_project_id = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project.clone(), cx)
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
Some(initial_project_id)
|
||||
@@ -260,9 +261,9 @@ impl Room {
|
||||
};
|
||||
|
||||
let did_join = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await;
|
||||
match did_join {
|
||||
@@ -310,7 +311,11 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Future<Output = ()> {
|
||||
let task = if self.status.is_online() {
|
||||
let leave = self.leave_internal(cx);
|
||||
Some(cx.background_executor().spawn(async move {
|
||||
@@ -338,19 +343,20 @@ impl Room {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Model<Self>> {
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
response.channel_id.map(ChannelId),
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = room.channel_id.is_none();
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
room.apply_room_update(room_proto, model, cx)?;
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(room)
|
||||
@@ -364,8 +370,8 @@ impl Room {
|
||||
&& self.pending_call_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
@@ -389,16 +395,16 @@ impl Room {
|
||||
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
|
||||
for project in self.shared_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.unshare(cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.unshare(model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
for project in self.joined_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.disconnected_from_host(cx);
|
||||
project.close(cx);
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.disconnected_from_host(model, cx);
|
||||
project.close(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -428,9 +434,9 @@ impl Room {
|
||||
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Rejoining;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
// Wait for client to re-establish a connection to the server.
|
||||
@@ -444,7 +450,8 @@ impl Room {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade() else { break };
|
||||
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
|
||||
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
|
||||
{
|
||||
Ok(task) => {
|
||||
if task.await.log_err().is_some() {
|
||||
return true;
|
||||
@@ -493,14 +500,15 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
|
||||
.await?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut reshared_projects = Vec::new();
|
||||
let mut rejoined_projects = Vec::new();
|
||||
@@ -548,27 +556,29 @@ impl Room {
|
||||
rejoined_projects,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = response.await?;
|
||||
let message_id = response.message_id;
|
||||
let response = response.payload;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)?;
|
||||
this.apply_room_update(room_proto, model, cx)?;
|
||||
|
||||
for reshared_project in response.reshared_projects {
|
||||
if let Some(project) = projects.get(&reshared_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.reshared(reshared_project, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.reshared(reshared_project, model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for rejoined_project in response.rejoined_projects {
|
||||
if let Some(project) = projects.get(&rejoined_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.rejoined(rejoined_project, message_id, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project
|
||||
.rejoined(rejoined_project, message_id, model, cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -626,12 +636,13 @@ impl Room {
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
role: proto::ChannelRole,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
let role = role.into();
|
||||
cx.spawn(|_, _| async move {
|
||||
cx.spawn(|_| async move {
|
||||
client
|
||||
.request(proto::SetRoomParticipantRole {
|
||||
room_id,
|
||||
@@ -703,13 +714,16 @@ impl Room {
|
||||
.payload
|
||||
.room
|
||||
.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.apply_room_update(room, model, cx)
|
||||
})?
|
||||
}
|
||||
|
||||
fn apply_room_update(
|
||||
&mut self,
|
||||
mut room: proto::Room,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
// Filter ourselves out from the room's participants.
|
||||
let local_participant_ix = room
|
||||
@@ -731,18 +745,18 @@ impl Room {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
self.user_store.update(cx, move |user_store, model, cx| {
|
||||
(
|
||||
user_store.get_users(remote_participant_user_ids, cx),
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
user_store.get_users(remote_participant_user_ids, model, cx),
|
||||
user_store.get_users(pending_participant_user_ids, model, cx),
|
||||
)
|
||||
});
|
||||
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
self.pending_room_update = Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.participant_user_ids.clear();
|
||||
|
||||
if let Some(participant) = local_participant {
|
||||
@@ -754,18 +768,20 @@ impl Room {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
for project in mem::take(&mut this.shared_projects) {
|
||||
if let Some(project) = project.upgrade() {
|
||||
this.unshare_project(project, cx).log_err();
|
||||
this.unshare_project(project, model, cx).log_err();
|
||||
}
|
||||
}
|
||||
this.local_participant.projects.clear();
|
||||
if let Some(live_kit_room) = &mut this.live_kit {
|
||||
live_kit_room.stop_publishing(cx);
|
||||
live_kit_room.stop_publishing(model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.set_role(role, model, cx)
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -799,20 +815,23 @@ impl Room {
|
||||
|
||||
for project in &participant.projects {
|
||||
if !old_projects.contains(&project.id) {
|
||||
cx.emit(Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update(cx, |project, model, cx| {
|
||||
if project.remote_id() == Some(*unshared_project_id) {
|
||||
project.disconnected_from_host(cx);
|
||||
project.disconnected_from_host(model, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -822,9 +841,12 @@ impl Room {
|
||||
false
|
||||
}
|
||||
});
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let role = participant.role();
|
||||
@@ -841,9 +863,12 @@ impl Room {
|
||||
{
|
||||
remote_participant.location = location;
|
||||
remote_participant.role = role;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.remote_participants.insert(
|
||||
@@ -876,6 +901,7 @@ impl Room {
|
||||
for track in video_tracks {
|
||||
this.live_kit_room_updated(
|
||||
RoomUpdate::SubscribedToRemoteVideoTrack(track),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
@@ -889,6 +915,7 @@ impl Room {
|
||||
track.clone(),
|
||||
publication.clone(),
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
@@ -902,9 +929,12 @@ impl Room {
|
||||
true
|
||||
} else {
|
||||
for project in &participant.projects {
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -942,26 +972,26 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
this.leave(cx).detach();
|
||||
this.leave(model, cx).detach();
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
this.user_store.update(cx, |user_store, model, cx| {
|
||||
let participant_indices_by_user_id = this
|
||||
.remote_participants
|
||||
.iter()
|
||||
.map(|(user_id, participant)| (*user_id, participant.participant_index))
|
||||
.collect();
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, cx);
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -979,7 +1009,8 @@ impl Room {
|
||||
fn live_kit_room_updated(
|
||||
&mut self,
|
||||
update: RoomUpdate,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
match update {
|
||||
RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
|
||||
@@ -990,9 +1021,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
|
||||
participant.video_tracks.insert(track_id.clone(), track);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::UnsubscribedFromRemoteVideoTrack {
|
||||
@@ -1005,9 +1039,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.video_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::ActiveSpeakersChanged { speakers } => {
|
||||
@@ -1061,9 +1098,12 @@ impl Room {
|
||||
participant.audio_tracks.insert(track_id.clone(), track);
|
||||
participant.muted = publication.is_muted();
|
||||
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::UnsubscribedFromRemoteAudioTrack {
|
||||
@@ -1076,9 +1116,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.audio_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::LocalAudioTrackUnpublished { publication } => {
|
||||
@@ -1104,7 +1147,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1132,17 +1175,18 @@ impl Room {
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project_id: Option<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
self.pending_call_count += 1;
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::Call {
|
||||
room_id,
|
||||
@@ -1150,10 +1194,10 @@ impl Room {
|
||||
initial_project_id,
|
||||
})
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_call_count -= 1;
|
||||
if this.should_leave() {
|
||||
this.leave(cx).detach_and_log_err(cx);
|
||||
this.leave(model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})?;
|
||||
result?;
|
||||
@@ -1166,16 +1210,17 @@ impl Room {
|
||||
id: u64,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let project =
|
||||
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
!project.read(cx).is_disconnected(cx)
|
||||
@@ -1192,7 +1237,8 @@ impl Room {
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some(project_id) = project.read(cx).remote_id() {
|
||||
return Task::ready(Ok(project_id));
|
||||
@@ -1204,19 +1250,19 @@ impl Room {
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
project.shared(response.project_id, cx)
|
||||
project.update(&mut cx, |project, model, cx| {
|
||||
project.shared(response.project_id, model, cx)
|
||||
})??;
|
||||
|
||||
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.shared_projects.insert(project.downgrade());
|
||||
let active_project = this.local_participant.active_project.as_ref();
|
||||
if active_project.map_or(false, |location| *location == project) {
|
||||
this.set_location(Some(&project), cx)
|
||||
this.set_location(Some(&project), model, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1230,7 +1276,8 @@ impl Room {
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
@@ -1238,10 +1285,11 @@ impl Room {
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))?;
|
||||
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
|
||||
|
||||
if self.local_participant.active_project == Some(project.downgrade()) {
|
||||
self.set_location(Some(&project), cx).detach_and_log_err(cx);
|
||||
self.set_location(Some(&project), model, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1249,7 +1297,8 @@ impl Room {
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
@@ -1273,7 +1322,7 @@ impl Room {
|
||||
proto::participant_location::Variant::External(proto::participant_location::External {})
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::UpdateParticipantLocation {
|
||||
@@ -1317,7 +1366,7 @@ impl Room {
|
||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||
}
|
||||
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
pub fn can_use_microphone(&self, _cx: &AppContext) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member | Talker => true,
|
||||
@@ -1334,7 +1383,11 @@ impl Room {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_microphone(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
@@ -1342,18 +1395,18 @@ impl Room {
|
||||
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.microphone_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
publish_id
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let track = LocalAudioTrack::create();
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, _| {
|
||||
.update(&mut cx, |this, model, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_audio_track(track))
|
||||
@@ -1364,7 +1417,7 @@ impl Room {
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1392,7 +1445,7 @@ impl Room {
|
||||
live_kit.microphone_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1401,7 +1454,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.microphone_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1410,7 +1463,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
} else if self.is_screen_sharing() {
|
||||
@@ -1420,13 +1473,13 @@ impl Room {
|
||||
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
(live_kit.room.display_sources(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let displays = displays.await?;
|
||||
let display = displays
|
||||
@@ -1435,7 +1488,7 @@ impl Room {
|
||||
let track = LocalVideoTrack::screen_share_for_display(display);
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, _| {
|
||||
.update(&mut cx, |this, model, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_video_track(track))
|
||||
@@ -1447,7 +1500,7 @@ impl Room {
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1470,7 +1523,7 @@ impl Room {
|
||||
live_kit.screen_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::StartScreenshare, cx);
|
||||
@@ -1482,7 +1535,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1491,7 +1544,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
// When unmuting, undeafen if the user was deafened before.
|
||||
let was_deafened = live_kit.deafened;
|
||||
@@ -1507,19 +1560,19 @@ impl Room {
|
||||
let muted = live_kit.muted_by_user;
|
||||
let should_undeafen = was_deafened && !live_kit.deafened;
|
||||
|
||||
if let Some(task) = self.set_mute(muted, cx) {
|
||||
if let Some(task) = self.set_mute(muted, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_undeafen {
|
||||
if let Some(task) = self.set_deafened(false, cx) {
|
||||
if let Some(task) = self.set_deafened(false, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
// When deafening, mute the microphone if it was not already muted.
|
||||
// When un-deafening, unmute the microphone, unless it was explicitly muted.
|
||||
@@ -1527,19 +1580,19 @@ impl Room {
|
||||
live_kit.deafened = deafened;
|
||||
let should_change_mute = !live_kit.muted_by_user;
|
||||
|
||||
if let Some(task) = self.set_deafened(deafened, cx) {
|
||||
if let Some(task) = self.set_deafened(deafened, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_change_mute {
|
||||
if let Some(task) = self.set_mute(deafened, cx) {
|
||||
if let Some(task) = self.set_mute(deafened, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
@@ -1551,14 +1604,14 @@ impl Room {
|
||||
match mem::take(&mut live_kit.screen_track) {
|
||||
LocalTrack::None => Err(anyhow!("screen was not shared")),
|
||||
LocalTrack::Pending { .. } => {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} => {
|
||||
live_kit.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||
Ok(())
|
||||
@@ -1569,10 +1622,11 @@ impl Room {
|
||||
fn set_deafened(
|
||||
&mut self,
|
||||
deafened: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
let mut track_updates = Vec::new();
|
||||
for participant in self.remote_participants.values() {
|
||||
@@ -1603,10 +1657,11 @@ impl Room {
|
||||
fn set_mute(
|
||||
&mut self,
|
||||
should_mute: bool,
|
||||
cx: &mut ModelContext<Room>,
|
||||
model: &Model<Room>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
if should_mute {
|
||||
Audio::play_sound(Sound::Mute, cx);
|
||||
@@ -1619,7 +1674,7 @@ impl Room {
|
||||
if should_mute {
|
||||
None
|
||||
} else {
|
||||
Some(self.share_microphone(cx))
|
||||
Some(self.share_microphone(model, cx))
|
||||
}
|
||||
}
|
||||
LocalTrack::Pending { .. } => None,
|
||||
@@ -1631,7 +1686,7 @@ impl Room {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
|
||||
pub fn set_display_sources(&self, sources: Vec<livekit_client_macos::MacOSDisplay>) {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -1641,7 +1696,7 @@ impl Room {
|
||||
}
|
||||
|
||||
struct LiveKitRoom {
|
||||
room: Arc<live_kit_client::Room>,
|
||||
room: Arc<livekit_client_macos::Room>,
|
||||
screen_track: LocalTrack,
|
||||
microphone_track: LocalTrack,
|
||||
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
|
||||
@@ -1654,13 +1709,13 @@ struct LiveKitRoom {
|
||||
}
|
||||
|
||||
impl LiveKitRoom {
|
||||
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
|
||||
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
|
||||
if let LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
if let LocalTrack::Published {
|
||||
@@ -1668,7 +1723,7 @@ impl LiveKitRoom {
|
||||
} = mem::replace(&mut self.screen_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore};
|
||||
use anyhow::Result;
|
||||
use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task};
|
||||
use language::proto::serialize_version;
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
@@ -62,17 +62,21 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let buffer = cx.new_model(|model, cx| {
|
||||
let capability = channel_store.read(cx).channel_capability(channel.id);
|
||||
language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text)
|
||||
})?;
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
|
||||
buffer.update(&mut cx, |buffer, model, cx| {
|
||||
buffer.apply_ops(operations, model, cx)
|
||||
})?;
|
||||
|
||||
let subscription = client.subscribe_to_entity(channel.id.0)?;
|
||||
|
||||
anyhow::Ok(cx.new_model(|cx| {
|
||||
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||
cx.on_release(Self::release).detach();
|
||||
anyhow::Ok(cx.new_model(|model, cx| {
|
||||
model
|
||||
.subscribe(&buffer, cx, Self::on_buffer_update)
|
||||
.detach();
|
||||
model.on_release(cx, Self::release).detach();
|
||||
let mut this = Self {
|
||||
buffer,
|
||||
buffer_epoch: response.epoch,
|
||||
@@ -81,11 +85,11 @@ impl ChannelBuffer {
|
||||
collaborators: Default::default(),
|
||||
acknowledge_task: None,
|
||||
channel_id: channel.id,
|
||||
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||
subscription: Some(subscription.set_model(model, &mut cx.to_async())),
|
||||
user_store,
|
||||
channel_store,
|
||||
};
|
||||
this.replace_collaborators(response.collaborators, cx);
|
||||
this.replace_collaborators(response.collaborators, model, cx);
|
||||
this
|
||||
})?)
|
||||
}
|
||||
@@ -114,7 +118,8 @@ impl ChannelBuffer {
|
||||
pub(crate) fn replace_collaborators(
|
||||
&mut self,
|
||||
collaborators: Vec<proto::Collaborator>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut new_collaborators = HashMap::default();
|
||||
for collaborator in collaborators {
|
||||
@@ -125,14 +130,14 @@ impl ChannelBuffer {
|
||||
|
||||
for (_, old_collaborator) in &self.collaborators {
|
||||
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id, cx)
|
||||
self.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id, model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
self.collaborators = new_collaborators;
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
async fn handle_update_channel_buffer(
|
||||
@@ -147,10 +152,10 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
this.buffer
|
||||
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
|
||||
.update(cx, |buffer, model, cx| buffer.apply_ops(ops, model, cx))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -161,10 +166,10 @@ impl ChannelBuffer {
|
||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.replace_collaborators(message.payload.collaborators, cx);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.replace_collaborators(message.payload.collaborators, model, cx);
|
||||
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -172,7 +177,8 @@ impl ChannelBuffer {
|
||||
&mut self,
|
||||
_: Model<language::Buffer>,
|
||||
event: &language::BufferEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
language::BufferEvent::Operation {
|
||||
@@ -195,20 +201,20 @@ impl ChannelBuffer {
|
||||
.log_err();
|
||||
}
|
||||
language::BufferEvent::Edited => {
|
||||
cx.emit(ChannelBufferEvent::BufferEdited);
|
||||
model.emit(ChannelBufferEvent::BufferEdited, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
|
||||
pub fn acknowledge_buffer_version(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let version = buffer.version();
|
||||
let buffer_id = buffer.remote_id().into();
|
||||
let client = self.client.clone();
|
||||
let epoch = self.epoch();
|
||||
|
||||
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
|
||||
self.acknowledge_task = Some(cx.spawn(move |cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
@@ -242,19 +248,19 @@ impl ChannelBuffer {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub(crate) fn disconnect(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
log::info!("channel buffer {} disconnected", self.channel_id);
|
||||
if self.connected {
|
||||
self.connected = false;
|
||||
self.subscription.take();
|
||||
cx.emit(ChannelBufferEvent::Disconnected);
|
||||
cx.notify()
|
||||
model.emit(ChannelBufferEvent::Disconnected, cx);
|
||||
model.notify(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(ChannelBufferEvent::ChannelChanged);
|
||||
cx.notify()
|
||||
pub(crate) fn channel_changed(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
model.emit(ChannelBufferEvent::ChannelChanged, cx);
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
|
||||
@@ -7,9 +7,7 @@ use client::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
|
||||
use rand::prelude::*;
|
||||
use rpc::AnyProtoClient;
|
||||
use std::{
|
||||
@@ -119,8 +117,8 @@ impl ChannelChat {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let handle = cx.new_model(|cx| {
|
||||
cx.on_release(Self::release).detach();
|
||||
let handle = cx.new_model(|model, cx| {
|
||||
model.on_release(cx, Self::release).detach();
|
||||
Self {
|
||||
channel_id: channel.id,
|
||||
user_store: user_store.clone(),
|
||||
@@ -134,7 +132,7 @@ impl ChannelChat {
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
|
||||
_subscription: subscription.set_model(model, &mut cx.to_async()),
|
||||
}
|
||||
})?;
|
||||
Self::handle_loaded_messages(
|
||||
@@ -171,7 +169,8 @@ impl ChannelChat {
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
message: MessageParams,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<Task<Result<u64>>> {
|
||||
if message.text.trim().is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
@@ -200,6 +199,7 @@ impl ChannelChat {
|
||||
},
|
||||
&(),
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
@@ -207,7 +207,7 @@ impl ChannelChat {
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
|
||||
// todo - handle messages that fail to send (e.g. >1024 chars)
|
||||
Ok(cx.spawn(move |this, mut cx| async move {
|
||||
Ok(model.spawn(cx, move |this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
@@ -221,8 +221,8 @@ impl ChannelChat {
|
||||
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
|
||||
let id = response.id;
|
||||
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
if this.first_loaded_message_id.is_none() {
|
||||
this.first_loaded_message_id = Some(id);
|
||||
}
|
||||
@@ -231,15 +231,20 @@ impl ChannelChat {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn remove_message(
|
||||
&mut self,
|
||||
id: u64,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let response = self.rpc.request(proto::RemoveChannelMessage {
|
||||
channel_id: self.channel_id.0,
|
||||
message_id: id,
|
||||
});
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
response.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(id, cx);
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_removed(id, model, cx);
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -249,13 +254,15 @@ impl ChannelChat {
|
||||
&mut self,
|
||||
id: u64,
|
||||
message: MessageParams,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
self.message_update(
|
||||
ChannelMessageId::Saved(id),
|
||||
message.text.clone(),
|
||||
message.mentions.clone(),
|
||||
Some(OffsetDateTime::now_utc()),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -268,13 +275,17 @@ impl ChannelChat {
|
||||
nonce: Some(nonce.into()),
|
||||
mentions: mentions_to_proto(&message.mentions),
|
||||
});
|
||||
Ok(cx.spawn(move |_, _| async move {
|
||||
Ok(cx.spawn(move |_| async move {
|
||||
request.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
|
||||
pub fn load_more_messages(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Option<()>>> {
|
||||
if self.loaded_all_messages {
|
||||
return None;
|
||||
}
|
||||
@@ -283,7 +294,7 @@ impl ChannelChat {
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
let before_message_id = self.first_loaded_message_id()?;
|
||||
Some(cx.spawn(move |this, mut cx| {
|
||||
Some(model.spawn(cx, move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
@@ -329,7 +340,7 @@ impl ChannelChat {
|
||||
) -> Option<usize> {
|
||||
loop {
|
||||
let step = chat
|
||||
.update(&mut cx, |chat, cx| {
|
||||
.update(&mut cx, |chat, model, cx| {
|
||||
if let Some(first_id) = chat.first_loaded_message_id() {
|
||||
if first_id <= message_id {
|
||||
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&());
|
||||
@@ -347,7 +358,7 @@ impl ChannelChat {
|
||||
);
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue(chat.load_more_messages(cx))
|
||||
ControlFlow::Continue(chat.load_more_messages(model, cx))
|
||||
})
|
||||
.log_err()?;
|
||||
match step {
|
||||
@@ -357,7 +368,7 @@ impl ChannelChat {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn acknowledge_last_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
|
||||
if self
|
||||
.last_acknowledged_id
|
||||
@@ -370,8 +381,8 @@ impl ChannelChat {
|
||||
})
|
||||
.ok();
|
||||
self.last_acknowledged_id = Some(latest_message_id);
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
|
||||
self.channel_store.update(cx, |store, model, cx| {
|
||||
store.acknowledge_message_id(self.channel_id, latest_message_id, model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -388,7 +399,7 @@ impl ChannelChat {
|
||||
let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
|
||||
|
||||
let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
|
||||
let loaded_message_ids = this.update(cx, |this, _| {
|
||||
let loaded_message_ids = this.update(cx, |this, model, _| {
|
||||
let mut loaded_message_ids: HashSet<u64> = HashSet::default();
|
||||
for message in loaded_messages.iter() {
|
||||
if let Some(saved_message_id) = message.id.into() {
|
||||
@@ -425,68 +436,69 @@ impl ChannelChat {
|
||||
.await?;
|
||||
Some(messages_from_proto(response.messages, &user_store, cx).await?)
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, model, cx| {
|
||||
this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(loaded_messages, cx);
|
||||
this.insert_messages(loaded_messages, model, cx);
|
||||
if let Some(loaded_ancestors) = loaded_ancestors {
|
||||
this.insert_messages(loaded_ancestors, cx);
|
||||
this.insert_messages(loaded_ancestors, model, cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::JoinChannelChat {
|
||||
channel_id: channel_id.0,
|
||||
})
|
||||
.await?;
|
||||
Self::handle_loaded_messages(
|
||||
this.clone(),
|
||||
user_store.clone(),
|
||||
rpc.clone(),
|
||||
response.messages,
|
||||
response.done,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, _| {
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
body: pending_message.body,
|
||||
mentions: mentions_to_proto(&pending_message.mentions),
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
reply_to_message_id: pending_message.reply_to_message_id,
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
model
|
||||
.spawn(cx, move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::JoinChannelChat {
|
||||
channel_id: channel_id.0,
|
||||
})
|
||||
.await?;
|
||||
Self::handle_loaded_messages(
|
||||
this.clone(),
|
||||
user_store.clone(),
|
||||
rpc.clone(),
|
||||
response.messages,
|
||||
response.done,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
let pending_messages = this.update(&mut cx, |this, _, _| {
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
body: pending_message.body,
|
||||
mentions: mentions_to_proto(&pending_message.mentions),
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
reply_to_message_id: pending_message.reply_to_message_id,
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
@@ -531,7 +543,7 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
@@ -539,12 +551,15 @@ impl ChannelChat {
|
||||
let message_id = message.id;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
cx.emit(ChannelChatEvent::NewMessage {
|
||||
channel_id: this.channel_id,
|
||||
message_id,
|
||||
})
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
model.emit(
|
||||
ChannelChatEvent::NewMessage {
|
||||
channel_id: this.channel_id,
|
||||
message_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -555,8 +570,8 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::RemoveChannelMessage>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(message.payload.message_id, cx)
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_removed(message.payload.message_id, model, cx)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -566,7 +581,7 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::ChannelMessageUpdate>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
@@ -574,19 +589,25 @@ impl ChannelChat {
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_update(
|
||||
message.id,
|
||||
message.body,
|
||||
message.mentions,
|
||||
message.edited_at,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
fn insert_messages(
|
||||
&mut self,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>(&())
|
||||
@@ -631,21 +652,27 @@ impl ChannelChat {
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
|
||||
fn message_removed(&mut self, id: u64, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
|
||||
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
|
||||
if let Some(item) = cursor.item() {
|
||||
@@ -658,7 +685,7 @@ impl ChannelChat {
|
||||
|
||||
// If the message that was deleted was the last acknowledged message,
|
||||
// replace the acknowledged message with an earlier one.
|
||||
self.channel_store.update(cx, |store, _| {
|
||||
self.channel_store.update(cx, |store, model, _| {
|
||||
let summary = self.messages.summary();
|
||||
if summary.count == 0 {
|
||||
store.set_acknowledged_message_id(self.channel_id, None);
|
||||
@@ -669,10 +696,13 @@ impl ChannelChat {
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: deleted_message_ix..deleted_message_ix + 1,
|
||||
new_count: 0,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: deleted_message_ix..deleted_message_ix + 1,
|
||||
new_count: 0,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,7 +713,8 @@ impl ChannelChat {
|
||||
body: String,
|
||||
mentions: Vec<(Range<usize>, u64)>,
|
||||
edited_at: Option<OffsetDateTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
|
||||
let mut messages = cursor.slice(&id, Bias::Left, &());
|
||||
@@ -701,12 +732,15 @@ impl ChannelChat {
|
||||
drop(cursor);
|
||||
self.messages = messages;
|
||||
|
||||
cx.emit(ChannelChatEvent::UpdateMessage {
|
||||
message_ix: ix,
|
||||
message_id: id,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::UpdateMessage {
|
||||
message_ix: ix,
|
||||
message_id: id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,8 +762,8 @@ impl ChannelMessage {
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
.update(cx, |user_store, model, cx| {
|
||||
user_store.get_user(message.sender_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -779,8 +813,8 @@ impl ChannelMessage {
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
.update(cx, |user_store, model, cx| {
|
||||
user_store.get_users(unique_user_ids, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, User
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
@@ -23,7 +23,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
cx.new_model(|model, cx| ChannelStore::new(client.clone(), user_store.clone(), model, cx));
|
||||
cx.set_global(GlobalChannelStore(channel_store));
|
||||
}
|
||||
|
||||
@@ -160,32 +160,37 @@ impl ChannelStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let rpc_subscriptions = [
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_channels),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_user_channels),
|
||||
];
|
||||
|
||||
let mut connection_status = client.status();
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
let watch_connection_status = cx.spawn(|this, mut cx| async move {
|
||||
let watch_connection_status = model.spawn(cx, |this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade()?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
this.update(&mut cx, |this, model, cx| this.handle_connect(model, cx))
|
||||
.ok()?
|
||||
.await
|
||||
.log_err()?;
|
||||
}
|
||||
client::Status::SignedOut | client::Status::UpgradeRequired => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx))
|
||||
.ok();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.handle_disconnect(false, model, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx))
|
||||
.ok();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.handle_disconnect(true, model, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,12 +210,12 @@ impl ChannelStore {
|
||||
_rpc_subscriptions: rpc_subscriptions,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
disconnect_channel_buffers_task: None,
|
||||
_update_channels: cx.spawn(|this, mut cx| async move {
|
||||
_update_channels: model.spawn(cx, |this, mut cx| async move {
|
||||
maybe!(async move {
|
||||
while let Some(update_channels) = update_channels_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
let update_task = this.update(&mut cx, |this, cx| {
|
||||
this.update_channels(update_channels, cx)
|
||||
let update_task = this.update(&mut cx, |this, model, cx| {
|
||||
this.update_channels(update_channels, model, cx)
|
||||
})?;
|
||||
if let Some(update_task) = update_task {
|
||||
update_task.await.log_err();
|
||||
@@ -307,15 +312,17 @@ impl ChannelStore {
|
||||
pub fn open_channel_buffer(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<ChannelBuffer>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_store = cx.handle();
|
||||
let channel_store = model.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_buffers,
|
||||
|channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -323,7 +330,8 @@ impl ChannelStore {
|
||||
pub fn fetch_channel_messages(
|
||||
&self,
|
||||
message_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ChannelMessage>>> {
|
||||
let request = if message_ids.is_empty() {
|
||||
None
|
||||
@@ -333,13 +341,13 @@ impl ChannelStore {
|
||||
.request(proto::GetChannelMessagesById { message_ids }),
|
||||
)
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
if let Some(request) = request {
|
||||
let response = request.await?;
|
||||
let this = this
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("channel store dropped"))?;
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
@@ -384,26 +392,28 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.acknowledge_message_id(message_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn update_latest_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.update_latest_message_id(message_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn acknowledge_notes_version(
|
||||
@@ -411,13 +421,14 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.acknowledge_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn update_latest_notes_version(
|
||||
@@ -425,27 +436,30 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.update_latest_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<ChannelChat>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let this = cx.handle();
|
||||
let this = model.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_chats,
|
||||
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -460,7 +474,8 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
|
||||
load: F,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<T>>>
|
||||
where
|
||||
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
|
||||
@@ -483,9 +498,9 @@ impl ChannelStore {
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
let task = cx
|
||||
.spawn(move |this, mut cx| async move {
|
||||
let channel = this.update(&mut cx, |this, _| {
|
||||
let task = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let channel = this.update(&mut cx, |this, _, _| {
|
||||
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
|
||||
Arc::new(anyhow!("no channel for id: {}", channel_id))
|
||||
})
|
||||
@@ -496,25 +511,26 @@ impl ChannelStore {
|
||||
.shared();
|
||||
|
||||
e.insert(OpenedModelHandle::Loading(task.clone()));
|
||||
cx.spawn({
|
||||
let task = task.clone();
|
||||
move |this, mut cx| async move {
|
||||
let result = task.await;
|
||||
this.update(&mut cx, |this, _| match result {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
get_map(this).remove(&channel_id);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
model
|
||||
.spawn(cx, {
|
||||
let task = task.clone();
|
||||
move |this, mut cx| async move {
|
||||
let result = task.await;
|
||||
this.update(&mut cx, |this, _, _| match result {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
get_map(this).remove(&channel_id);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
break task;
|
||||
}
|
||||
}
|
||||
@@ -572,11 +588,12 @@ impl ChannelStore {
|
||||
&self,
|
||||
name: &str,
|
||||
parent_id: Option<ChannelId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<ChannelId>> {
|
||||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches('#').to_owned();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::CreateChannel {
|
||||
name,
|
||||
@@ -589,12 +606,13 @@ impl ChannelStore {
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
let channel_id = ChannelId(channel.id);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
@@ -603,7 +621,7 @@ impl ChannelStore {
|
||||
// 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));
|
||||
model.emit(ChannelEvent::ChannelCreated(channel_id), cx);
|
||||
})?;
|
||||
|
||||
Ok(channel_id)
|
||||
@@ -614,10 +632,11 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
to: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |_, _| async move {
|
||||
cx.spawn(move |_| async move {
|
||||
let _ = client
|
||||
.request(proto::MoveChannel {
|
||||
channel_id: channel_id.0,
|
||||
@@ -633,10 +652,11 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |_, _| async move {
|
||||
cx.spawn(move |_| async move {
|
||||
let _ = client
|
||||
.request(proto::SetChannelVisibility {
|
||||
channel_id: channel_id.0,
|
||||
@@ -653,15 +673,16 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id: channel_id.0,
|
||||
@@ -670,9 +691,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
result?;
|
||||
@@ -685,15 +706,16 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::RemoveChannelMember {
|
||||
channel_id: channel_id.0,
|
||||
@@ -701,9 +723,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result?;
|
||||
Ok(())
|
||||
@@ -715,15 +737,16 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("member request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberRole {
|
||||
channel_id: channel_id.0,
|
||||
@@ -732,9 +755,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
result?;
|
||||
@@ -746,11 +769,12 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
new_name: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let name = new_name.to_string();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::RenameChannel {
|
||||
channel_id: channel_id.0,
|
||||
@@ -759,12 +783,13 @@ impl ChannelStore {
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
@@ -773,7 +798,7 @@ impl ChannelStore {
|
||||
// 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))
|
||||
model.emit(ChannelEvent::ChannelRenamed(channel_id), cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -783,7 +808,8 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
accept: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -801,11 +827,12 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
query: String,
|
||||
limit: u16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ChannelMembership>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.downgrade();
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::GetChannelMembers {
|
||||
channel_id: channel_id.0,
|
||||
@@ -813,7 +840,7 @@ impl ChannelStore {
|
||||
limit: limit as u64,
|
||||
})
|
||||
.await?;
|
||||
user_store.update(&mut cx, |user_store, _| {
|
||||
user_store.update(&mut cx, |user_store, model, _| {
|
||||
user_store.insert(response.users);
|
||||
response
|
||||
.members
|
||||
@@ -855,7 +882,7 @@ impl ChannelStore {
|
||||
message: TypedEnvelope<proto::UpdateChannels>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
this.update_channels_tx
|
||||
.unbounded_send(message.payload)
|
||||
.unwrap();
|
||||
@@ -868,13 +895,14 @@ impl ChannelStore {
|
||||
message: TypedEnvelope<proto::UpdateUserChannels>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for buffer_version in message.payload.observed_channel_buffer_version {
|
||||
let version = language::proto::deserialize_version(&buffer_version.version);
|
||||
this.acknowledge_notes_version(
|
||||
ChannelId(buffer_version.channel_id),
|
||||
buffer_version.epoch,
|
||||
&version,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -882,6 +910,7 @@ impl ChannelStore {
|
||||
this.acknowledge_message_id(
|
||||
ChannelId(message_id.channel_id),
|
||||
message_id.message_id,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -896,7 +925,7 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn handle_connect(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
self.channel_participants.clear();
|
||||
@@ -907,8 +936,8 @@ impl ChannelStore {
|
||||
for chat in self.opened_chats.values() {
|
||||
if let OpenedModelHandle::Open(chat) = chat {
|
||||
if let Some(chat) = chat.upgrade() {
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.rejoin(cx);
|
||||
chat.update(cx, |chat, model, cx| {
|
||||
chat.rejoin(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -937,17 +966,17 @@ impl ChannelStore {
|
||||
buffers: buffer_versions,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let mut response = response.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.opened_buffers.retain(|_, buffer| match buffer {
|
||||
OpenedModelHandle::Open(channel_buffer) => {
|
||||
let Some(channel_buffer) = channel_buffer.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
channel_buffer.update(cx, |channel_buffer, cx| {
|
||||
channel_buffer.update(cx, |channel_buffer, model, cx| {
|
||||
let channel_id = channel_buffer.channel_id;
|
||||
if let Some(remote_buffer) = response
|
||||
.buffers
|
||||
@@ -960,12 +989,13 @@ impl ChannelStore {
|
||||
|
||||
channel_buffer.replace_collaborators(
|
||||
mem::take(&mut remote_buffer.collaborators),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
|
||||
let operations = channel_buffer
|
||||
.buffer()
|
||||
.update(cx, |buffer, cx| {
|
||||
.update(cx, |buffer, model, cx| {
|
||||
let outgoing_operations =
|
||||
buffer.serialize_ops(Some(remote_version), cx);
|
||||
let incoming_operations =
|
||||
@@ -973,7 +1003,7 @@ impl ChannelStore {
|
||||
.into_iter()
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
buffer.apply_ops(incoming_operations, cx);
|
||||
buffer.apply_ops(incoming_operations, model, cx);
|
||||
anyhow::Ok(outgoing_operations)
|
||||
})
|
||||
.log_err();
|
||||
@@ -999,7 +1029,7 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
channel_buffer.disconnect(cx);
|
||||
channel_buffer.disconnect(model, cx);
|
||||
false
|
||||
})
|
||||
}
|
||||
@@ -1011,21 +1041,28 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
cx.notify();
|
||||
fn handle_disconnect(
|
||||
&mut self,
|
||||
wait_for_reconnect: bool,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
model.notify(cx);
|
||||
self.did_subscribe = false;
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
if wait_for_reconnect {
|
||||
cx.background_executor().timer(RECONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.disconnect(model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1076,8 @@ impl ChannelStore {
|
||||
pub(crate) fn update_channels(
|
||||
&mut self,
|
||||
payload: proto::UpdateChannels,
|
||||
cx: &mut ModelContext<ChannelStore>,
|
||||
model: &Model<ChannelStore>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if !payload.remove_channel_invitations.is_empty() {
|
||||
self.channel_invitations
|
||||
@@ -1127,7 +1165,7 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
if payload.channel_participants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -1142,13 +1180,13 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
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 = self.user_store.update(cx, |user_store, model, cx| {
|
||||
user_store.get_users(all_user_ids, model, cx)
|
||||
});
|
||||
Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let users = users.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for entry in &channel_participants {
|
||||
let mut participants: Vec<_> = entry
|
||||
.participant_user_ids
|
||||
@@ -1167,7 +1205,7 @@ impl ChannelStore {
|
||||
.insert(ChannelId(entry.channel_id), participants);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
let user_id = 5;
|
||||
let channel_id = 5;
|
||||
let channel_store = cx.update(init_test);
|
||||
let client = channel_store.update(cx, |s, _| s.client());
|
||||
let client = channel_store.update(cx, |s, model, _| s.client());
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
// Get the available channels.
|
||||
@@ -169,9 +169,9 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_store.update(cx, |store, cx| {
|
||||
let channel = channel_store.update(cx, |store, model, cx| {
|
||||
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
||||
store.open_channel_chat(channel_id, cx)
|
||||
store.open_channel_chat(channel_id, model, cx)
|
||||
});
|
||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||
server.respond(
|
||||
@@ -221,7 +221,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let channel = channel.await.unwrap();
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
@@ -270,7 +270,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
@@ -281,8 +281,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
channel.load_more_messages(cx).unwrap().detach();
|
||||
channel.update(cx, |channel, model, cx| {
|
||||
channel.load_more_messages(model, cx).unwrap().detach();
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
@@ -323,7 +323,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
@@ -346,7 +346,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let user_store = cx.new_model(|model, cx| UserStore::new(client.clone(), model, cx));
|
||||
|
||||
client::init(&client, cx);
|
||||
crate::init(&client, user_store, cx);
|
||||
@@ -359,7 +359,9 @@ fn update_channels(
|
||||
message: proto::UpdateChannels,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
|
||||
let task = channel_store.update(cx, |store, model, cx| {
|
||||
store.update_channels(message, model, cx)
|
||||
});
|
||||
assert!(task.is_none());
|
||||
}
|
||||
|
||||
@@ -369,7 +371,7 @@ fn assert_channels(
|
||||
expected_channels: &[(usize, String)],
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let actual = channel_store.update(cx, |store, _| {
|
||||
let actual = channel_store.update(cx, |store, model, _| {
|
||||
store
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string()))
|
||||
|
||||
@@ -1962,7 +1962,7 @@ mod tests {
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
AnyProtoClient::from(client.clone()).add_model_message_handler(
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match model.update(&mut cx, |model, _| model.id).unwrap() {
|
||||
match model.update(&mut cx, |model, _, _| model.id).unwrap() {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
2 => done_tx2.try_send(()).unwrap(),
|
||||
_ => unreachable!(),
|
||||
@@ -1970,15 +1970,15 @@ mod tests {
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
let model1 = cx.new_model(|_| TestModel {
|
||||
let model1 = cx.new_model(|_, _| TestModel {
|
||||
id: 1,
|
||||
subscription: None,
|
||||
});
|
||||
let model2 = cx.new_model(|_| TestModel {
|
||||
let model2 = cx.new_model(|_, _| TestModel {
|
||||
id: 2,
|
||||
subscription: None,
|
||||
});
|
||||
let model3 = cx.new_model(|_| TestModel {
|
||||
let model3 = cx.new_model(|_, _| TestModel {
|
||||
id: 3,
|
||||
subscription: None,
|
||||
});
|
||||
@@ -2018,7 +2018,7 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
let model = cx.new_model(|_, _| TestModel::default());
|
||||
let (done_tx1, _done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
let subscription1 = client.add_message_handler(
|
||||
@@ -2053,20 +2053,20 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
let model = cx.new_model(|_, _| TestModel::default());
|
||||
let (done_tx, mut done_rx) = smol::channel::unbounded();
|
||||
let subscription = client.add_message_handler(
|
||||
model.clone().downgrade(),
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
|
||||
model
|
||||
.update(&mut cx, |model, _| model.subscription.take())
|
||||
.update(&mut cx, |this, model, _| this.subscription.take())
|
||||
.unwrap();
|
||||
done_tx.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
model.update(cx, |model, _| {
|
||||
model.subscription = Some(subscription);
|
||||
model.update(cx, |this, model, _| {
|
||||
this.subscription = Some(subscription);
|
||||
});
|
||||
server.send(proto::Ping {});
|
||||
done_rx.next().await.unwrap();
|
||||
|
||||
@@ -204,7 +204,7 @@ impl FakeServer {
|
||||
client: Arc<Client>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Model<UserStore> {
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
|
||||
let user_store = cx.new_model(|model, cx| UserStore::new(client, model, cx));
|
||||
assert_eq!(
|
||||
self.receive::<proto::GetUsers>()
|
||||
.await
|
||||
|
||||
@@ -5,8 +5,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{channel::mpsc, Future, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUri, Task,
|
||||
WeakModel,
|
||||
AppContext, AsyncAppContext, EventEmitter, Model, SharedString, SharedUri, Task, WeakModel,
|
||||
};
|
||||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
@@ -136,14 +135,14 @@ enum UpdateContacts {
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> Self {
|
||||
pub fn new(client: Arc<Client>, model: &Model<Self>, cx: &AppContext) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
|
||||
let rpc_subscriptions = vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_plan),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_plan),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_contacts),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_invite_info),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_show_contacts),
|
||||
];
|
||||
Self {
|
||||
users: Default::default(),
|
||||
@@ -158,19 +157,19 @@ impl UserStore {
|
||||
invite_info: None,
|
||||
client: Arc::downgrade(&client),
|
||||
update_contacts_tx,
|
||||
_maintain_contacts: cx.spawn(|this, mut cx| async move {
|
||||
_maintain_contacts: model.spawn(cx, |this, mut cx| async move {
|
||||
let _subscriptions = rpc_subscriptions;
|
||||
while let Some(message) = update_contacts_rx.next().await {
|
||||
if let Ok(task) =
|
||||
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
|
||||
{
|
||||
if let Ok(task) = this.update(&mut cx, |this, model, cx| {
|
||||
this.update_contacts(message, model, cx)
|
||||
}) {
|
||||
task.log_err().await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
_maintain_current_user: cx.spawn(|this, mut cx| async move {
|
||||
_maintain_current_user: model.spawn(cx, |this, mut cx| async move {
|
||||
let mut status = client.status();
|
||||
let weak = Arc::downgrade(&client);
|
||||
drop(client);
|
||||
@@ -183,8 +182,8 @@ impl UserStore {
|
||||
Status::Connected { .. } => {
|
||||
if let Some(user_id) = client.user_id() {
|
||||
let fetch_user = if let Ok(fetch_user) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.get_user(user_id, cx).log_err()
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
this.get_user(user_id, model, cx).log_err()
|
||||
}) {
|
||||
fetch_user
|
||||
} else {
|
||||
@@ -206,7 +205,7 @@ impl UserStore {
|
||||
staff,
|
||||
);
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.update(cx, |this, model, _| {
|
||||
this.set_current_user_accepted_tos_at(
|
||||
info.accepted_tos_at,
|
||||
);
|
||||
@@ -218,20 +217,20 @@ impl UserStore {
|
||||
|
||||
current_user_tx.send(user).await.ok();
|
||||
|
||||
this.update(&mut cx, |_, cx| cx.notify())?;
|
||||
this.update(&mut cx, |_, model, cx| model.notify(cx))?;
|
||||
}
|
||||
}
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
this.clear_contacts()
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
Status::ConnectionLost => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
this.clear_contacts()
|
||||
})?
|
||||
.await;
|
||||
@@ -242,7 +241,7 @@ impl UserStore {
|
||||
Ok(())
|
||||
}),
|
||||
pending_contact_requests: Default::default(),
|
||||
weak_self: cx.weak_model(),
|
||||
weak_self: model.downgrade(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,12 +256,12 @@ impl UserStore {
|
||||
message: TypedEnvelope<proto::UpdateInviteInfo>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.invite_info = Some(InviteInfo {
|
||||
url: Arc::from(message.payload.url),
|
||||
count: message.payload.count,
|
||||
});
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -272,7 +271,7 @@ impl UserStore {
|
||||
_: TypedEnvelope<proto::ShowContacts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
|
||||
this.update(&mut cx, |_, model, cx| model.emit(Event::ShowContacts, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -285,7 +284,7 @@ impl UserStore {
|
||||
message: TypedEnvelope<proto::UpdateContacts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
this.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Update(message.payload))
|
||||
.unwrap();
|
||||
@@ -298,9 +297,9 @@ impl UserStore {
|
||||
message: TypedEnvelope<proto::UpdateUserPlan>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.current_plan = Some(message.payload.plan());
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -308,7 +307,8 @@ impl UserStore {
|
||||
fn update_contacts(
|
||||
&mut self,
|
||||
message: UpdateContacts,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
match message {
|
||||
UpdateContacts::Wait(barrier) => {
|
||||
@@ -330,8 +330,8 @@ impl UserStore {
|
||||
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
||||
user_ids.extend(message.outgoing_requests.iter());
|
||||
|
||||
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let load_users = self.get_users(user_ids.into_iter().collect(), model, cx);
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
load_users.await?;
|
||||
|
||||
// Users are fetched in parallel above and cached in call to get_users
|
||||
@@ -349,8 +349,8 @@ impl UserStore {
|
||||
let mut incoming_requests = Vec::new();
|
||||
for request in message.incoming_requests {
|
||||
incoming_requests.push({
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.get_user(request.requester_id, cx)
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.get_user(request.requester_id, model, cx)
|
||||
})?
|
||||
.await?
|
||||
});
|
||||
@@ -359,8 +359,10 @@ impl UserStore {
|
||||
let mut outgoing_requests = Vec::new();
|
||||
for requested_user_id in message.outgoing_requests {
|
||||
outgoing_requests.push(
|
||||
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
|
||||
.await?,
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.get_user(requested_user_id, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,7 +373,7 @@ impl UserStore {
|
||||
let removed_outgoing_requests =
|
||||
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
// Remove contacts
|
||||
this.contacts
|
||||
.retain(|contact| !removed_contacts.contains(&contact.user.id));
|
||||
@@ -389,10 +391,13 @@ impl UserStore {
|
||||
// Remove incoming contact requests
|
||||
this.incoming_contact_requests.retain(|user| {
|
||||
if removed_incoming_requests.contains(&user.id) {
|
||||
cx.emit(Event::Contact {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Cancelled,
|
||||
});
|
||||
model.emit(
|
||||
Event::Contact {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Cancelled,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -425,7 +430,7 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -483,17 +488,24 @@ impl UserStore {
|
||||
pub fn request_contact(
|
||||
&mut self,
|
||||
responder_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
|
||||
self.perform_contact_request(
|
||||
responder_id,
|
||||
proto::RequestContact { responder_id },
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn remove_contact(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
|
||||
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, model, cx)
|
||||
}
|
||||
|
||||
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
|
||||
@@ -506,7 +518,8 @@ impl UserStore {
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
accept: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(
|
||||
requester_id,
|
||||
@@ -518,6 +531,7 @@ impl UserStore {
|
||||
proto::ContactRequestResponse::Decline
|
||||
} as i32,
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -525,10 +539,11 @@ impl UserStore {
|
||||
pub fn dismiss_contact_request(
|
||||
&self,
|
||||
requester_id: u64,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
cx.spawn(move |_, _| async move {
|
||||
cx.spawn(move |_| async move {
|
||||
client
|
||||
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||
.request(proto::RespondToContactRequest {
|
||||
@@ -544,18 +559,19 @@ impl UserStore {
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
request: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let response = client
|
||||
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||
.request(request)
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Entry::Occupied(mut request_count) =
|
||||
this.pending_contact_requests.entry(user_id)
|
||||
{
|
||||
@@ -564,7 +580,7 @@ impl UserStore {
|
||||
request_count.remove();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
response?;
|
||||
Ok(())
|
||||
@@ -594,25 +610,27 @@ impl UserStore {
|
||||
pub fn get_users(
|
||||
&self,
|
||||
user_ids: Vec<u64>,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let mut user_ids_to_fetch = user_ids.clone();
|
||||
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
if !user_ids_to_fetch.is_empty() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.load_users(
|
||||
proto::GetUsers {
|
||||
user_ids: user_ids_to_fetch,
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
user_ids
|
||||
.iter()
|
||||
.map(|user_id| {
|
||||
@@ -629,33 +647,44 @@ impl UserStore {
|
||||
pub fn fuzzy_search_users(
|
||||
&self,
|
||||
query: String,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||
self.load_users(proto::FuzzySearchUsers { query }, model, cx)
|
||||
}
|
||||
|
||||
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
|
||||
self.users.get(&user_id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext<Self>) -> Option<Arc<User>> {
|
||||
pub fn get_user_optimistic(
|
||||
&self,
|
||||
user_id: u64,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Option<Arc<User>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return Some(user);
|
||||
}
|
||||
|
||||
self.get_user(user_id, cx).detach_and_log_err(cx);
|
||||
self.get_user(user_id, model, cx).detach_and_log_err(cx);
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_user(&self, user_id: u64, cx: &ModelContext<Self>) -> Task<Result<Arc<User>>> {
|
||||
pub fn get_user(
|
||||
&self,
|
||||
user_id: u64,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Arc<User>>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return Task::ready(Ok(user));
|
||||
}
|
||||
|
||||
let load_users = self.get_users(vec![user_id], cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let load_users = self.get_users(vec![user_id], model, cx);
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
load_users.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
this.users
|
||||
.get(&user_id)
|
||||
.cloned()
|
||||
@@ -687,20 +716,24 @@ impl UserStore {
|
||||
.map(|accepted_tos_at| accepted_tos_at.is_some())
|
||||
}
|
||||
|
||||
pub fn accept_terms_of_service(&self, cx: &ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn accept_terms_of_service(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.current_user().is_none() {
|
||||
return Task::ready(Err(anyhow!("no current user")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let response = client
|
||||
.request(proto::AcceptTermsOfService {})
|
||||
.await
|
||||
.context("error accepting tos")?;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at))
|
||||
})
|
||||
} else {
|
||||
@@ -718,15 +751,16 @@ impl UserStore {
|
||||
fn load_users(
|
||||
&self,
|
||||
request: impl RequestMessage<Response = UsersResponse>,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
if let Some(rpc) = client.upgrade() {
|
||||
let response = rpc.request(request).await.context("error loading users")?;
|
||||
let users = response.users;
|
||||
|
||||
this.update(&mut cx, |this, _| this.insert(users))
|
||||
this.update(&mut cx, |this, _, _| this.insert(users))
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -752,11 +786,12 @@ impl UserStore {
|
||||
pub fn set_participant_indices(
|
||||
&mut self,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if participant_indices != self.participant_indices {
|
||||
self.participant_indices = participant_indices;
|
||||
cx.emit(Event::ParticipantIndicesChanged);
|
||||
model.emit(Event::ParticipantIndicesChanged, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,8 +816,10 @@ impl UserStore {
|
||||
if !missing_user_ids.is_empty() {
|
||||
let this = self.weak_self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
this.update(&mut cx, |this, cx| this.get_users(missing_user_ids, cx))?
|
||||
.await
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.get_users(missing_user_ids, model, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -807,8 +844,8 @@ impl Contact {
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let user = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(contact.user_id, cx)
|
||||
.update(cx, |user_store, model, cx| {
|
||||
user_store.get_user(contact.user_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(Self {
|
||||
|
||||
@@ -5,9 +5,9 @@ HTTP_PORT = 8080
|
||||
API_TOKEN = "secret"
|
||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||
ZED_ENVIRONMENT = "development"
|
||||
LIVE_KIT_SERVER = "http://localhost:7880"
|
||||
LIVE_KIT_KEY = "devkey"
|
||||
LIVE_KIT_SECRET = "secret"
|
||||
LIVEKIT_SERVER = "http://localhost:7880"
|
||||
LIVEKIT_KEY = "devkey"
|
||||
LIVEKIT_SECRET = "secret"
|
||||
BLOB_STORE_ACCESS_KEY = "the-blob-store-access-key"
|
||||
BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
|
||||
BLOB_STORE_BUCKET = "the-extensions-bucket"
|
||||
|
||||
@@ -40,7 +40,7 @@ google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
http_client.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
livekit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
@@ -77,6 +77,12 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
livekit_client_macos = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
livekit_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
assistant_tool.workspace = true
|
||||
@@ -101,7 +107,6 @@ hyper.workspace = true
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
live_kit_client = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
menu.workspace = true
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
@@ -125,5 +130,11 @@ util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dev-dependencies]
|
||||
livekit_client_macos = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
|
||||
livekit_client = {workspace = true, features = ["test-support"] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["async-stripe"]
|
||||
|
||||
@@ -109,17 +109,17 @@ spec:
|
||||
secretKeyRef:
|
||||
name: zed-client
|
||||
key: checksum-seed
|
||||
- name: LIVE_KIT_SERVER
|
||||
- name: LIVEKIT_SERVER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: server
|
||||
- name: LIVE_KIT_KEY
|
||||
- name: LIVEKIT_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: key
|
||||
- name: LIVE_KIT_SECRET
|
||||
- name: LIVEKIT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
|
||||
@@ -9,6 +9,7 @@ use collections::HashSet;
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::ActiveValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
use stripe::{
|
||||
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
|
||||
@@ -19,6 +20,7 @@ use stripe::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{
|
||||
@@ -124,6 +126,20 @@ async fn update_billing_preferences(
|
||||
.await?
|
||||
};
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Spend Limit Updated",
|
||||
Some(user.metrics_id),
|
||||
user.admin,
|
||||
None,
|
||||
json!({
|
||||
"user_id": user.id,
|
||||
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
|
||||
}),
|
||||
)
|
||||
.write(&app.kinesis_client, &app.config.kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
rpc_server.refresh_llm_tokens_for_user(user.id).await;
|
||||
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
|
||||
@@ -154,9 +154,9 @@ impl Database {
|
||||
}
|
||||
let role = role.unwrap();
|
||||
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
let livekit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
let room_id = self
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &tx)
|
||||
.get_or_create_channel_room(channel_id, &livekit_room, &tx)
|
||||
.await?;
|
||||
|
||||
self.join_channel_room_internal(room_id, user_id, connection, role, &tx)
|
||||
@@ -896,7 +896,7 @@ impl Database {
|
||||
pub(crate) async fn get_or_create_channel_room(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
live_kit_room: &str,
|
||||
livekit_room: &str,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<RoomId> {
|
||||
let room = room::Entity::find()
|
||||
@@ -909,7 +909,7 @@ impl Database {
|
||||
} else {
|
||||
let result = room::Entity::insert(room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel_id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
live_kit_room: ActiveValue::Set(livekit_room.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(tx)
|
||||
|
||||
@@ -103,11 +103,11 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
livekit_room: &str,
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
live_kit_room: ActiveValue::set(livekit_room.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@@ -1316,7 +1316,7 @@ impl Database {
|
||||
channel,
|
||||
proto::Room {
|
||||
id: db_room.id.to_proto(),
|
||||
live_kit_room: db_room.live_kit_room,
|
||||
livekit_room: db_room.live_kit_room,
|
||||
participants: participants.into_values().collect(),
|
||||
pending_participants,
|
||||
followers,
|
||||
|
||||
@@ -156,9 +156,9 @@ pub struct Config {
|
||||
pub clickhouse_password: Option<String>,
|
||||
pub clickhouse_database: Option<String>,
|
||||
pub invite_link_prefix: String,
|
||||
pub live_kit_server: Option<String>,
|
||||
pub live_kit_key: Option<String>,
|
||||
pub live_kit_secret: Option<String>,
|
||||
pub livekit_server: Option<String>,
|
||||
pub livekit_key: Option<String>,
|
||||
pub livekit_secret: Option<String>,
|
||||
pub llm_database_url: Option<String>,
|
||||
pub llm_database_max_connections: Option<u32>,
|
||||
pub llm_database_migrations_path: Option<PathBuf>,
|
||||
@@ -210,9 +210,9 @@ impl Config {
|
||||
database_max_connections: 0,
|
||||
api_token: "".into(),
|
||||
invite_link_prefix: "".into(),
|
||||
live_kit_server: None,
|
||||
live_kit_key: None,
|
||||
live_kit_secret: None,
|
||||
livekit_server: None,
|
||||
livekit_key: None,
|
||||
livekit_secret: None,
|
||||
llm_database_url: None,
|
||||
llm_database_max_connections: None,
|
||||
llm_database_migrations_path: None,
|
||||
@@ -277,7 +277,7 @@ impl ServiceMode {
|
||||
pub struct AppState {
|
||||
pub db: Arc<Database>,
|
||||
pub llm_db: Option<Arc<LlmDatabase>>,
|
||||
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
pub livekit_client: Option<Arc<dyn livekit_server::api::Client>>,
|
||||
pub blob_store_client: Option<aws_sdk_s3::Client>,
|
||||
pub stripe_client: Option<Arc<stripe::Client>>,
|
||||
pub stripe_billing: Option<Arc<StripeBilling>>,
|
||||
@@ -309,17 +309,17 @@ impl AppState {
|
||||
None
|
||||
};
|
||||
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
let livekit_client = if let Some(((server, key), secret)) = config
|
||||
.livekit_server
|
||||
.as_ref()
|
||||
.zip(config.live_kit_key.as_ref())
|
||||
.zip(config.live_kit_secret.as_ref())
|
||||
.zip(config.livekit_key.as_ref())
|
||||
.zip(config.livekit_secret.as_ref())
|
||||
{
|
||||
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
|
||||
Some(Arc::new(livekit_server::api::LiveKitClient::new(
|
||||
server.clone(),
|
||||
key.clone(),
|
||||
secret.clone(),
|
||||
)) as Arc<dyn live_kit_server::api::Client>)
|
||||
)) as Arc<dyn livekit_server::api::Client>)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -329,7 +329,7 @@ impl AppState {
|
||||
let this = Self {
|
||||
db: db.clone(),
|
||||
llm_db,
|
||||
live_kit_client,
|
||||
livekit_client,
|
||||
blob_store_client: build_blob_store_client(&config).await.log_err(),
|
||||
stripe_billing: stripe_client
|
||||
.clone()
|
||||
|
||||
@@ -309,6 +309,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
@@ -418,7 +419,7 @@ impl Server {
|
||||
let peer = self.peer.clone();
|
||||
let timeout = self.app_state.executor.sleep(CLEANUP_TIMEOUT);
|
||||
let pool = self.connection_pool.clone();
|
||||
let live_kit_client = self.app_state.live_kit_client.clone();
|
||||
let livekit_client = self.app_state.livekit_client.clone();
|
||||
|
||||
let span = info_span!("start server");
|
||||
self.app_state.executor.spawn_detached(
|
||||
@@ -463,8 +464,8 @@ impl Server {
|
||||
for room_id in room_ids {
|
||||
let mut contacts_to_update = HashSet::default();
|
||||
let mut canceled_calls_to_user_ids = Vec::new();
|
||||
let mut live_kit_room = String::new();
|
||||
let mut delete_live_kit_room = false;
|
||||
let mut livekit_room = String::new();
|
||||
let mut delete_livekit_room = false;
|
||||
|
||||
if let Some(mut refreshed_room) = app_state
|
||||
.db
|
||||
@@ -487,8 +488,8 @@ impl Server {
|
||||
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
|
||||
canceled_calls_to_user_ids =
|
||||
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
|
||||
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
|
||||
delete_live_kit_room = refreshed_room.room.participants.is_empty();
|
||||
livekit_room = mem::take(&mut refreshed_room.room.livekit_room);
|
||||
delete_livekit_room = refreshed_room.room.participants.is_empty();
|
||||
}
|
||||
|
||||
{
|
||||
@@ -539,9 +540,9 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(live_kit) = live_kit_client.as_ref() {
|
||||
if delete_live_kit_room {
|
||||
live_kit.delete_room(live_kit_room).await.trace_err();
|
||||
if let Some(live_kit) = livekit_client.as_ref() {
|
||||
if delete_livekit_room {
|
||||
live_kit.delete_room(livekit_room).await.trace_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1210,15 +1211,15 @@ async fn create_room(
|
||||
response: Response<proto::CreateRoom>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let live_kit_room = nanoid::nanoid!(30);
|
||||
let livekit_room = nanoid::nanoid!(30);
|
||||
|
||||
let live_kit_connection_info = util::maybe!(async {
|
||||
let live_kit = session.app_state.live_kit_client.as_ref();
|
||||
let live_kit = session.app_state.livekit_client.as_ref();
|
||||
let live_kit = live_kit?;
|
||||
let user_id = session.user_id().to_string();
|
||||
|
||||
let token = live_kit
|
||||
.room_token(&live_kit_room, &user_id.to_string())
|
||||
.room_token(&livekit_room, &user_id.to_string())
|
||||
.trace_err()?;
|
||||
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
@@ -1232,7 +1233,7 @@ async fn create_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id(), session.connection_id, &live_kit_room)
|
||||
.create_room(session.user_id(), session.connection_id, &livekit_room)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
@@ -1284,22 +1285,22 @@ async fn join_room(
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
let live_kit_connection_info =
|
||||
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
|
||||
live_kit
|
||||
.room_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&session.user_id().to_string(),
|
||||
)
|
||||
.trace_err()
|
||||
.map(|token| proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
can_publish: true,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let live_kit_connection_info = if let Some(live_kit) = session.app_state.livekit_client.as_ref()
|
||||
{
|
||||
live_kit
|
||||
.room_token(
|
||||
&joined_room.room.livekit_room,
|
||||
&session.user_id().to_string(),
|
||||
)
|
||||
.trace_err()
|
||||
.map(|token| proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
can_publish: true,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(joined_room.room),
|
||||
@@ -1506,7 +1507,7 @@ async fn set_room_participant_role(
|
||||
let user_id = UserId::from_proto(request.user_id);
|
||||
let role = ChannelRole::from(request.role());
|
||||
|
||||
let (live_kit_room, can_publish) = {
|
||||
let (livekit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
@@ -1518,18 +1519,18 @@ async fn set_room_participant_role(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let livekit_room = room.livekit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
(livekit_room, can_publish)
|
||||
};
|
||||
|
||||
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
|
||||
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
|
||||
live_kit
|
||||
.update_participant(
|
||||
live_kit_room.clone(),
|
||||
livekit_room.clone(),
|
||||
request.user_id.to_string(),
|
||||
live_kit_server::proto::ParticipantPermission {
|
||||
livekit_server::proto::ParticipantPermission {
|
||||
can_subscribe: true,
|
||||
can_publish,
|
||||
can_publish_data: can_publish,
|
||||
@@ -3091,7 +3092,7 @@ async fn join_channel_internal(
|
||||
let live_kit_connection_info =
|
||||
session
|
||||
.app_state
|
||||
.live_kit_client
|
||||
.livekit_client
|
||||
.as_ref()
|
||||
.and_then(|live_kit| {
|
||||
let (can_publish, token) = if role == ChannelRole::Guest {
|
||||
@@ -3099,7 +3100,7 @@ async fn join_channel_internal(
|
||||
false,
|
||||
live_kit
|
||||
.guest_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&joined_room.room.livekit_room,
|
||||
&session.user_id().to_string(),
|
||||
)
|
||||
.trace_err()?,
|
||||
@@ -3109,7 +3110,7 @@ async fn join_channel_internal(
|
||||
true,
|
||||
live_kit
|
||||
.room_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&joined_room.room.livekit_room,
|
||||
&session.user_id().to_string(),
|
||||
)
|
||||
.trace_err()?,
|
||||
@@ -4313,8 +4314,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
|
||||
|
||||
let room_id;
|
||||
let canceled_calls_to_user_ids;
|
||||
let live_kit_room;
|
||||
let delete_live_kit_room;
|
||||
let livekit_room;
|
||||
let delete_livekit_room;
|
||||
let room;
|
||||
let channel;
|
||||
|
||||
@@ -4327,8 +4328,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
|
||||
|
||||
room_id = RoomId::from_proto(left_room.room.id);
|
||||
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);
|
||||
delete_live_kit_room = left_room.deleted;
|
||||
livekit_room = mem::take(&mut left_room.room.livekit_room);
|
||||
delete_livekit_room = left_room.deleted;
|
||||
room = mem::take(&mut left_room.room);
|
||||
channel = mem::take(&mut left_room.channel);
|
||||
|
||||
@@ -4368,14 +4369,14 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
|
||||
update_user_contacts(contact_user_id, session).await?;
|
||||
}
|
||||
|
||||
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
|
||||
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
|
||||
live_kit
|
||||
.remove_participant(live_kit_room.clone(), session.user_id().to_string())
|
||||
.remove_participant(livekit_room.clone(), session.user_id().to_string())
|
||||
.await
|
||||
.trace_err();
|
||||
|
||||
if delete_live_kit_room {
|
||||
live_kit.delete_room(live_kit_room).await.trace_err();
|
||||
if delete_livekit_room {
|
||||
live_kit.delete_room(livekit_room).await.trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// todo(windows): Actually run the tests
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use call::Room;
|
||||
|
||||
@@ -9,7 +9,7 @@ use collab_ui::channel_view::ChannelView;
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Editor, ToOffset};
|
||||
use futures::future;
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext};
|
||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||
use serde_json::json;
|
||||
use std::ops::Range;
|
||||
@@ -161,43 +161,43 @@ async fn test_channel_notes_participant_indices(
|
||||
|
||||
// Clients A, B, and C open the channel notes
|
||||
let channel_view_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_c = cx_c
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Clients A, B, and C all insert and select some text
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
editor.insert("a", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("b", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![1..2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
channel_view_c.update(cx_c, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("c", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
@@ -207,8 +207,8 @@ async fn test_channel_notes_participant_indices(
|
||||
// in a call together.
|
||||
executor.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], model, cx);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,19 +223,21 @@ async fn test_channel_notes_participant_indices(
|
||||
// still doesn't have a color.
|
||||
executor.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -270,12 +272,12 @@ async fn test_channel_notes_participant_indices(
|
||||
.unwrap();
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
@@ -283,10 +285,10 @@ async fn test_channel_notes_participant_indices(
|
||||
|
||||
// Clients A and B see each other with the same colors as in the channel notes.
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], model, cx);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], model, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,9 +296,10 @@ async fn test_channel_notes_participant_indices(
|
||||
fn assert_remote_selections(
|
||||
editor: &mut Editor,
|
||||
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
|
||||
cx: &mut ViewContext<Editor>,
|
||||
model: &Model<Editor>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let snapshot = editor.snapshot(model, cx);
|
||||
let range = Anchor::min()..Anchor::max();
|
||||
let remote_selections = snapshot
|
||||
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
|
||||
@@ -344,7 +347,7 @@ async fn test_multiple_handles_to_channel_buffer(
|
||||
assert_eq!(channel_buffer, channel_buffer_3);
|
||||
|
||||
channel_buffer.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "hello")], None, cx);
|
||||
})
|
||||
});
|
||||
@@ -365,7 +368,7 @@ async fn test_multiple_handles_to_channel_buffer(
|
||||
.unwrap();
|
||||
assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
|
||||
channel_buffer.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, _| {
|
||||
buffer.buffer().update(cx, |buffer, model, _| {
|
||||
assert_eq!(buffer.text(), "hello");
|
||||
})
|
||||
});
|
||||
@@ -465,7 +468,7 @@ async fn test_rejoin_channel_buffer(
|
||||
.unwrap();
|
||||
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "1")], None, cx);
|
||||
})
|
||||
});
|
||||
@@ -477,12 +480,12 @@ async fn test_rejoin_channel_buffer(
|
||||
|
||||
// Both clients make an edit.
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(1..1, "2")], None, cx);
|
||||
})
|
||||
});
|
||||
channel_buffer_b.update(cx_b, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "0")], None, cx);
|
||||
})
|
||||
});
|
||||
@@ -552,7 +555,7 @@ async fn test_channel_buffers_and_server_restarts(
|
||||
.unwrap();
|
||||
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "1")], None, cx);
|
||||
})
|
||||
});
|
||||
@@ -567,12 +570,12 @@ async fn test_channel_buffers_and_server_restarts(
|
||||
|
||||
// While the server is down, both clients make an edit.
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(1..1, "2")], None, cx);
|
||||
})
|
||||
});
|
||||
channel_buffer_b.update(cx_b, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "0")], None, cx);
|
||||
})
|
||||
});
|
||||
@@ -642,8 +645,8 @@ async fn test_channel_buffer_changes(
|
||||
|
||||
// Closing the buffer should re-enable change tracking
|
||||
cx_b.update(|cx| {
|
||||
workspace_b.update(cx, |workspace, cx| {
|
||||
workspace.close_all_items_and_panes(&Default::default(), cx)
|
||||
workspace_b.update(cx, |workspace, model, cx| {
|
||||
workspace.close_all_items_and_panes(&Default::default(), model, cx)
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
@@ -107,7 +107,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
|
||||
});
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
@@ -116,7 +118,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
// B is promoted
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
call.room().unwrap().update(cx, |room, model, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
@@ -133,7 +135,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
|
||||
});
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||
cx_a.run_until_parked();
|
||||
@@ -142,7 +146,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
// B is demoted
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
call.room().unwrap().update(cx, |room, model, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Guest,
|
||||
@@ -226,13 +230,15 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
|
||||
});
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
call.room().unwrap().update(cx, |room, model, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
@@ -244,13 +250,15 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.unwrap_err();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
|
||||
});
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
call.room().unwrap().update(cx, |room, model, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Talker,
|
||||
@@ -262,7 +270,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
|
||||
});
|
||||
|
||||
// User B signs the zed CLA.
|
||||
server
|
||||
@@ -275,7 +285,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
// A can now grant write access to B.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
call.room().unwrap().update(cx, |room, model, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
@@ -287,5 +297,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
cx_b.update(|cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,8 +82,15 @@ async fn test_host_disconnect(
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
let workspace_b = cx_b
|
||||
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let workspace_b = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
None,
|
||||
project_b.clone(),
|
||||
client_b.app_state.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
|
||||
|
||||
@@ -98,7 +105,7 @@ async fn test_host_disconnect(
|
||||
.unwrap();
|
||||
|
||||
//TODO: focus
|
||||
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
|
||||
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(window)));
|
||||
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
|
||||
cx_b.update(|cx| {
|
||||
@@ -201,7 +208,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), model, cx));
|
||||
|
||||
let mut editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
@@ -216,7 +223,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), model, cx));
|
||||
|
||||
let mut editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
@@ -317,8 +324,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
|
||||
let editor_b = cx_b
|
||||
.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), model, cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
@@ -329,7 +336,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
|
||||
// Type a completion trigger character as the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(".", cx);
|
||||
});
|
||||
cx_b.focus_view(&editor_b);
|
||||
@@ -442,7 +449,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
// Now we do a second completion, this time to ensure that documentation/snippets are
|
||||
// resolved
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([46..46]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([46..46]));
|
||||
editor.handle_input("; a", cx);
|
||||
editor.handle_input(".", cx);
|
||||
});
|
||||
@@ -594,7 +601,7 @@ async fn test_collaborating_with_code_actions(
|
||||
|
||||
// Move cursor to a location that contains code actions.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
editor.change_selections(None, model, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
|
||||
});
|
||||
});
|
||||
@@ -675,7 +682,7 @@ async fn test_collaborating_with_code_actions(
|
||||
// Confirming the code action will trigger a resolve request.
|
||||
let confirm_action = editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
|
||||
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, model, cx)
|
||||
})
|
||||
.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
|
||||
@@ -796,7 +803,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Move cursor to a location that can be renamed.
|
||||
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([7..7]));
|
||||
editor.rename(&Rename, cx).unwrap()
|
||||
});
|
||||
|
||||
@@ -821,14 +828,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
|
||||
6..9
|
||||
);
|
||||
rename.editor.update(cx, |rename_editor, cx| {
|
||||
rename.editor.update(cx, |rename_editor, model, cx| {
|
||||
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
||||
assert_eq!(
|
||||
rename_selection.range(),
|
||||
0..3,
|
||||
"Rename that was triggered from zero selection caret, should propose the whole word."
|
||||
);
|
||||
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
||||
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
|
||||
rename_buffer.edit([(0..3, "THREE")], None, cx);
|
||||
});
|
||||
});
|
||||
@@ -839,7 +846,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
editor.cancel(&editor::actions::Cancel, cx);
|
||||
});
|
||||
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([7..8]));
|
||||
editor.rename(&Rename, cx).unwrap()
|
||||
});
|
||||
|
||||
@@ -863,21 +870,21 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let lsp_rename_start = rename.range.start.to_offset(&buffer);
|
||||
let lsp_rename_end = rename.range.end.to_offset(&buffer);
|
||||
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
|
||||
rename.editor.update(cx, |rename_editor, cx| {
|
||||
rename.editor.update(cx, |rename_editor, model, cx| {
|
||||
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
||||
assert_eq!(
|
||||
rename_selection.range(),
|
||||
1..2,
|
||||
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
|
||||
);
|
||||
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
||||
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
|
||||
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
|
||||
Editor::confirm_rename(editor, &ConfirmRename, model, cx).unwrap()
|
||||
});
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
|
||||
@@ -1191,7 +1198,7 @@ async fn test_share_project(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, model, cx));
|
||||
|
||||
// Client A sees client B's selection
|
||||
executor.run_until_parked();
|
||||
@@ -1296,7 +1303,8 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
|
||||
let editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), model, cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
@@ -1330,7 +1338,7 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_a.focus_view(&editor_a);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(">", cx);
|
||||
});
|
||||
|
||||
@@ -1416,7 +1424,8 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
|
||||
let editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), model, cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
@@ -1424,7 +1433,7 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_b.focus_view(&editor_b);
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(":", cx);
|
||||
});
|
||||
|
||||
@@ -1668,7 +1677,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
|
||||
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.handle_input(":", cx);
|
||||
});
|
||||
cx_b.focus_view(&editor_b);
|
||||
@@ -1693,7 +1702,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
|
||||
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input("a change to increment both buffers' versions", cx);
|
||||
});
|
||||
cx_a.focus_view(&editor_a);
|
||||
@@ -2070,7 +2079,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
let entries = blame.update(cx, |blame, model, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2086,7 +2095,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
]
|
||||
);
|
||||
|
||||
blame.update(cx, |blame, _| {
|
||||
blame.update(cx, |blame, model, _| {
|
||||
for (idx, entry) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(entry).unwrap();
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
@@ -2109,7 +2118,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
let entries = blame.update(cx, |blame, model, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2136,7 +2145,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
let entries = blame.update(cx, |blame, model, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2206,9 +2215,9 @@ async fn test_collaborating_with_editorconfig(
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let main_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
|
||||
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), model, cx));
|
||||
let other_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
|
||||
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), model, cx));
|
||||
let mut main_editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
@@ -2238,9 +2247,9 @@ async fn test_collaborating_with_editorconfig(
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let main_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
|
||||
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), model, cx));
|
||||
let other_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
|
||||
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), model, cx));
|
||||
let mut main_editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![allow(clippy::reversed_empty_ranges)]
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use crate::tests::TestServer;
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::ChannelId;
|
||||
use collab_ui::{
|
||||
@@ -8,21 +8,15 @@ use collab_ui::{
|
||||
};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{
|
||||
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
|
||||
View, VisualContext, VisualTestContext,
|
||||
point, AppContext, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString,
|
||||
TestAppContext, View, VisualContext, VisualTestContext,
|
||||
};
|
||||
use language::Capability;
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use project::WorktreeSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
item::{test::TestItem, ItemHandle as _},
|
||||
shared_screen::SharedScreen,
|
||||
SplitDirection, Workspace,
|
||||
};
|
||||
use workspace::{item::ItemHandle as _, SplitDirection, Workspace};
|
||||
|
||||
use super::TestClient;
|
||||
|
||||
@@ -279,12 +273,12 @@ async fn test_basic_following(
|
||||
|
||||
// When client A opens a multibuffer, client B does so as well.
|
||||
let multibuffer_a = cx_a.new_model(|cx| {
|
||||
let buffer_a1 = project_a.update(cx, |project, cx| {
|
||||
let buffer_a1 = project_a.update(cx, |project, model, cx| {
|
||||
project
|
||||
.get_open_buffer(&(worktree_id, "1.txt").into(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
let buffer_a2 = project_a.update(cx, |project, cx| {
|
||||
let buffer_a2 = project_a.update(cx, |project, model, cx| {
|
||||
project
|
||||
.get_open_buffer(&(worktree_id, "2.txt").into(), cx)
|
||||
.unwrap()
|
||||
@@ -296,6 +290,7 @@ async fn test_basic_following(
|
||||
context: 0..3,
|
||||
primary: None,
|
||||
}],
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
result.push_excerpts(
|
||||
@@ -304,15 +299,16 @@ async fn test_basic_following(
|
||||
context: 4..7,
|
||||
primary: None,
|
||||
}],
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
result
|
||||
});
|
||||
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
|
||||
let editor = cx.new_model(|model, cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, model, cx)
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, model, cx);
|
||||
editor
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -373,7 +369,7 @@ async fn test_basic_following(
|
||||
|
||||
// Changes to client A's editor are reflected on client B.
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
@@ -388,7 +384,7 @@ async fn test_basic_following(
|
||||
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([3..3]));
|
||||
editor.set_scroll_position(point(0., 100.), cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -428,106 +424,118 @@ async fn test_basic_following(
|
||||
editor_a1.item_id()
|
||||
);
|
||||
|
||||
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
let display = MacOSDisplay::new();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
use crate::rpc::RECONNECT_TIMEOUT;
|
||||
use gpui::TestScreenCaptureSource;
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
item::test::TestItem,
|
||||
shared_screen::SharedScreen,
|
||||
};
|
||||
|
||||
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
let display = TestScreenCaptureSource::new();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.set_screen_capture_sources(vec![display]);
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, model, cx| room.share_screen(cx))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.expect("no active item")
|
||||
.downcast::<SharedScreen>()
|
||||
.expect("active item isn't a shared screen")
|
||||
});
|
||||
.await
|
||||
.unwrap(); // This is what breaks
|
||||
executor.run_until_parked();
|
||||
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.expect("no active item")
|
||||
.downcast::<SharedScreen>()
|
||||
.expect("active item isn't a shared screen")
|
||||
});
|
||||
|
||||
// Client B activates Zed again, which causes the previous editor to become focused again.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
// Client B activates Zed again, which causes the previous editor to become focused again.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
editor_a1.item_id()
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_item(&multibuffer_editor_b, true, true, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
multibuffer_editor_a.item_id()
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.add_panel(panel, cx);
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
editor_a1.item_id()
|
||||
)
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.item_id()),
|
||||
shared_screen.item_id()
|
||||
);
|
||||
|
||||
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_item(&multibuffer_editor_b, true, true, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
// Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
multibuffer_editor_a.item_id()
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates an item that doesn't implement following,
|
||||
// so the previously-opened screen-sharing item gets activated.
|
||||
let unfollowable_item = cx_b.new_view(TestItem::new);
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
multibuffer_editor_a.item_id()
|
||||
)
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.item_id()),
|
||||
shared_screen.item_id()
|
||||
);
|
||||
|
||||
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.add_panel(panel, cx);
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.item_id()),
|
||||
shared_screen.item_id()
|
||||
);
|
||||
|
||||
// Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
// Following interrupts when client B disconnects.
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().item_id(),
|
||||
multibuffer_editor_a.item_id()
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates an item that doesn't implement following,
|
||||
// so the previously-opened screen-sharing item gets activated.
|
||||
let unfollowable_item = cx_b.new_view(TestItem::new);
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.item_id()),
|
||||
shared_screen.item_id()
|
||||
);
|
||||
|
||||
// Following interrupts when client B disconnects.
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
None
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -599,8 +607,8 @@ async fn test_following_tab_order(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
let pane_paths = |pane: &Model<workspace::Pane>, cx: &mut VisualTestContext| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
pane.items()
|
||||
.map(|item| {
|
||||
item.project_path(cx)
|
||||
@@ -918,7 +926,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// Client B focuses a file that they previously followed A to, breaking
|
||||
// the follow.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
@@ -969,7 +977,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// Client B closes tabs, some of which were originally opened by client A,
|
||||
// and some of which were originally opened by client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.close_inactive_items(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
@@ -1022,7 +1030,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
executor.run_until_parked();
|
||||
// Client A cycles through some tabs.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
@@ -1066,7 +1074,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
@@ -1113,7 +1121,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
workspace.active_pane().update(cx, |pane, model, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
@@ -1602,7 +1610,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// b should follow a to position 1
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1612,17 +1620,18 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
|
||||
// a unshares the project
|
||||
cx_a.update(|cx| {
|
||||
// todo! why do i need the annotation
|
||||
cx_a.update(|cx: &mut AppContext| {
|
||||
let project = workspace_a.read(cx).project().clone();
|
||||
ActiveCall::global(cx).update(cx, |call, cx| {
|
||||
call.unshare_project(project, cx).unwrap();
|
||||
ActiveCall::global(cx).update(cx, |call, model, cx| {
|
||||
call.unshare_project(project, model, cx).unwrap();
|
||||
})
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// b should not follow a to position 2
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
|
||||
editor.change_selections(None, model, cx, |s| s.select_ranges([2..2]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1770,11 +1779,11 @@ async fn test_following_into_excluded_file(
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::View<ProjectSharedNotification>> {
|
||||
) -> Vec<gpui::Model<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window
|
||||
.update(cx, |window, _| {
|
||||
.update(cx, |window, model, _| {
|
||||
if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
|
||||
ret.push(handle)
|
||||
}
|
||||
@@ -1815,8 +1824,8 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec
|
||||
})
|
||||
}
|
||||
|
||||
fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
fn pane_summaries(workspace: &Model<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
|
||||
workspace.update(cx, |workspace, model, cx| {
|
||||
let active_pane = workspace.active_pane();
|
||||
workspace
|
||||
.panes()
|
||||
@@ -1918,14 +1927,14 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 1.
|
||||
let channel_notes_1_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_1_a.update(cx_a, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
editor.insert("Hello from A.", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.change_selections(None, model, cx, |selections| {
|
||||
selections.select_ranges(vec![3..4]);
|
||||
});
|
||||
});
|
||||
@@ -1957,7 +1966,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
});
|
||||
channel_notes_1_b.update(cx_b, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
notes.editor.update(cx, |editor, model, cx| {
|
||||
assert_eq!(editor.text(cx), "Hello from A.");
|
||||
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
|
||||
})
|
||||
@@ -1965,7 +1974,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 2.
|
||||
let channel_notes_2_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_2_a.update(cx_a, |notes, cx| {
|
||||
@@ -2021,12 +2030,12 @@ pub(crate) async fn join_channel(
|
||||
}
|
||||
|
||||
async fn share_workspace(
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> anyhow::Result<u64> {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
|
||||
cx.read(ActiveCall::global)
|
||||
.update(cx, |call, cx| call.share_project(project, cx))
|
||||
.update(cx, |call, model, cx| call.share_project(project, model, cx))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -2065,7 +2074,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
|
||||
cx_a2.update(|cx| cx.activate_window());
|
||||
cx_a2
|
||||
.update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
|
||||
.update(|cx| ChannelView::open(channel, None, workspace_a2, model, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a2.run_until_parked();
|
||||
|
||||
@@ -25,7 +25,6 @@ use language::{
|
||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
use project::lsp_store::FormatTarget;
|
||||
@@ -241,56 +240,60 @@ async fn test_basic_calls(
|
||||
}
|
||||
);
|
||||
|
||||
// User A shares their screen
|
||||
let display = MacOSDisplay::new();
|
||||
let events_b = active_call_events(cx_b);
|
||||
let events_c = active_call_events(cx_c);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// User A shares their screen
|
||||
let display = gpui::TestScreenCaptureSource::new();
|
||||
let events_b = active_call_events(cx_b);
|
||||
let events_c = active_call_events(cx_c);
|
||||
cx_a.set_screen_capture_sources(vec![display]);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, model, cx| room.share_screen(cx))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
executor.run_until_parked();
|
||||
|
||||
// User B observes the remote screen sharing track.
|
||||
assert_eq!(events_b.borrow().len(), 1);
|
||||
let event_b = events_b.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
// User B observes the remote screen sharing track.
|
||||
assert_eq!(events_b.borrow().len(), 1);
|
||||
let event_b = events_b.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
}
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
}
|
||||
|
||||
// User C observes the remote screen sharing track.
|
||||
assert_eq!(events_c.borrow().len(), 1);
|
||||
let event_c = events_c.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
// User C observes the remote screen sharing track.
|
||||
assert_eq!(events_c.borrow().len(), 1);
|
||||
let event_c = events_c.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
|
||||
room_c.read_with(cx_c, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
room_c.read_with(cx_c, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
}
|
||||
}
|
||||
|
||||
// User A leaves the room.
|
||||
@@ -329,7 +332,7 @@ async fn test_basic_calls(
|
||||
// to automatically leave the room. User C leaves the room as well because
|
||||
// nobody else is in there.
|
||||
server
|
||||
.test_live_kit_server
|
||||
.test_livekit_server
|
||||
.disconnect_client(client_b.user_id().unwrap().to_string())
|
||||
.await;
|
||||
executor.run_until_parked();
|
||||
@@ -844,7 +847,7 @@ async fn test_client_disconnecting_from_room(
|
||||
// User B gets disconnected from the LiveKit server, which causes it
|
||||
// to automatically leave the room.
|
||||
server
|
||||
.test_live_kit_server
|
||||
.test_livekit_server
|
||||
.disconnect_client(client_b.user_id().unwrap().to_string())
|
||||
.await;
|
||||
executor.run_until_parked();
|
||||
@@ -1943,7 +1946,7 @@ async fn test_mute_deafen(
|
||||
room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
|
||||
|
||||
// Users A and B are both muted.
|
||||
// Users A and B are both unmuted.
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_a, cx_a),
|
||||
&[ParticipantAudioState {
|
||||
@@ -2075,7 +2078,17 @@ async fn test_mute_deafen(
|
||||
audio_tracks_playing: participant
|
||||
.audio_tracks
|
||||
.values()
|
||||
.map(|track| track.is_playing())
|
||||
.map({
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|track| track.is_playing()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
|(track, _)| track.rtc_track().enabled()
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2561,19 +2574,23 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_a = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@@ -2585,25 +2602,30 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_a = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text of the open buffer
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
@@ -2611,40 +2633,35 @@ async fn test_git_diff_base_change(
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
//Nested git dir
|
||||
|
||||
// Nested git dir
|
||||
let diff_base = "
|
||||
one
|
||||
three
|
||||
@@ -2667,19 +2684,23 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_b = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@@ -2691,25 +2712,29 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_b = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
@@ -2717,43 +2742,30 @@ async fn test_git_diff_base_change(
|
||||
|
||||
// Wait for buffer_local_b to receive it
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
println!("{:?}", buffer.as_rope().to_string());
|
||||
println!("{:?}", buffer.diff_base());
|
||||
println!(
|
||||
"{:?}",
|
||||
buffer
|
||||
.snapshot()
|
||||
.git_diff_hunks_in_row_range(0..4)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
@@ -6016,6 +6028,8 @@ async fn test_contact_requests(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_join_call_after_screen_was_shared(
|
||||
executor: BackgroundExecutor,
|
||||
@@ -6058,13 +6072,13 @@ async fn test_join_call_after_screen_was_shared(
|
||||
assert_eq!(call_b.calling_user.github_login, "user_a");
|
||||
|
||||
// User A shares their screen
|
||||
let display = MacOSDisplay::new();
|
||||
let display = gpui::TestScreenCaptureSource::new();
|
||||
cx_a.set_screen_capture_sources(vec![display]);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
})
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, model, cx| room.share_screen(cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6159,11 +6173,11 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
|
||||
let (workspace, cx) = client.build_test_workspace(cx).await;
|
||||
|
||||
cx.simulate_keystrokes("cmd-n");
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.update(cx, |workspace, model, cx| {
|
||||
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
|
||||
});
|
||||
cx.simulate_keystrokes("cmd-k left");
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.update(cx, |workspace, model, cx| {
|
||||
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
|
||||
});
|
||||
cx.simulate_keystrokes("cmd-k");
|
||||
@@ -6171,7 +6185,7 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
|
||||
// to verify that it doesn't fire in this case.
|
||||
cx.executor().advance_clock(Duration::from_secs(2));
|
||||
cx.simulate_keystrokes("left");
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.update(cx, |workspace, model, cx| {
|
||||
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
|
||||
});
|
||||
}
|
||||
@@ -6193,9 +6207,9 @@ async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppCont
|
||||
async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
let (_server, client) = TestServer::start1(cx).await;
|
||||
let (workspace, cx) = client.build_test_workspace(cx).await;
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
let worktree_id = project.update(cx, |project, model, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
@@ -6212,7 +6226,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
path: Path::new("3.rs").into(),
|
||||
};
|
||||
|
||||
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
|
||||
|
||||
let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
|
||||
pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
|
||||
@@ -6220,13 +6234,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Opening item 3 as a "permanent" tab
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
workspace.open_path(path_3.clone(), None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(pane.preview_item_id(), None);
|
||||
@@ -6237,13 +6251,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_1.clone());
|
||||
@@ -6258,13 +6272,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 2 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_2.clone());
|
||||
@@ -6279,11 +6293,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Going back should show item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_1.clone());
|
||||
@@ -6297,7 +6311,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Closing item 1
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
pane.close_item_by_id(
|
||||
pane.active_item().unwrap().item_id(),
|
||||
workspace::SaveIntent::Skip,
|
||||
@@ -6307,7 +6321,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(pane.preview_item_id(), None);
|
||||
@@ -6318,11 +6332,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Going back should show item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_3.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_1.clone());
|
||||
@@ -6336,14 +6350,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Close permanent tab
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
let id = pane.items().next().unwrap().item_id();
|
||||
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(
|
||||
@@ -6356,13 +6370,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
});
|
||||
|
||||
let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let right_pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(
|
||||
@@ -6374,7 +6388,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
assert!(pane.can_navigate_forward());
|
||||
});
|
||||
|
||||
right_pane.update(cx, |pane, cx| {
|
||||
right_pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(pane.preview_item_id(), None);
|
||||
@@ -6385,13 +6399,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 2 as preview in right pane
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(
|
||||
@@ -6403,7 +6417,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
assert!(pane.can_navigate_forward());
|
||||
});
|
||||
|
||||
right_pane.update(cx, |pane, cx| {
|
||||
right_pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_2.clone());
|
||||
@@ -6417,19 +6431,19 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Focus left pane
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.update(cx, |workspace, model, cx| {
|
||||
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
|
||||
});
|
||||
|
||||
// Open item 2 as preview in left pane
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 1);
|
||||
assert_eq!(get_path(pane, 0, cx), path_2.clone());
|
||||
assert_eq!(
|
||||
@@ -6441,7 +6455,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
assert!(!pane.can_navigate_forward());
|
||||
});
|
||||
|
||||
right_pane.update(cx, |pane, cx| {
|
||||
right_pane.update(cx, |pane, model, cx| {
|
||||
assert_eq!(pane.items_len(), 2);
|
||||
assert_eq!(get_path(pane, 0, cx), path_1.clone());
|
||||
assert_eq!(get_path(pane, 1, cx), path_2.clone());
|
||||
@@ -6531,12 +6545,12 @@ async fn test_context_collaboration_with_reconnect(
|
||||
|
||||
// Host and guest make changes
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
context.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "Host change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
context.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "Guest change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
@@ -6554,12 +6568,12 @@ async fn test_context_collaboration_with_reconnect(
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
server.forbid_connections();
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
context.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "Host offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
context.buffer().update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
@@ -6629,7 +6643,7 @@ async fn test_remote_git_branches(
|
||||
executor.run_until_parked();
|
||||
|
||||
let branches_b = cx_b
|
||||
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
|
||||
.update(|cx| project_b.update(cx, |project, model, cx| project.branches(root_path.clone(), cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -6643,7 +6657,7 @@ async fn test_remote_git_branches(
|
||||
assert_eq!(&branches_b, &branches);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project_b.update(cx, |project, model, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||
})
|
||||
})
|
||||
@@ -6653,8 +6667,8 @@ async fn test_remote_git_branches(
|
||||
executor.run_until_parked();
|
||||
|
||||
let host_branch = cx_a.update(|cx| {
|
||||
project_a.update(cx, |project, cx| {
|
||||
project.worktree_store().update(cx, |worktree_store, cx| {
|
||||
project_a.update(cx, |project, model, cx| {
|
||||
project.worktree_store().update(cx, |worktree_store, model, cx| {
|
||||
worktree_store
|
||||
.current_branch(root_path.clone(), cx)
|
||||
.unwrap()
|
||||
@@ -6666,7 +6680,7 @@ async fn test_remote_git_branches(
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project_b.update(cx, |project, model, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||
})
|
||||
})
|
||||
@@ -6676,8 +6690,8 @@ async fn test_remote_git_branches(
|
||||
executor.run_until_parked();
|
||||
|
||||
let host_branch = cx_a.update(|cx| {
|
||||
project_a.update(cx, |project, cx| {
|
||||
project.worktree_store().update(cx, |worktree_store, cx| {
|
||||
project_a.update(cx, |project, model, cx| {
|
||||
project.worktree_store().update(cx, |worktree_store, model, cx| {
|
||||
worktree_store.current_branch(root_path, cx).unwrap()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,14 +21,14 @@ async fn test_notifications(
|
||||
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||
client_a.notification_store().update(cx_a, |_, cx| {
|
||||
let events = notification_events_a.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
cx.subscribe(model, move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
client_b.notification_store().update(cx_b, |_, cx| {
|
||||
let events = notification_events_b.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
cx.subscribe(model, move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
|
||||
@@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
) -> Result<(), TestError> {
|
||||
match operation {
|
||||
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
|
||||
let buffer = client.channel_store().update(cx, |store, cx| {
|
||||
let buffer = client.channel_store().update(cx, |store, model, cx| {
|
||||
let channel_id = store
|
||||
.ordered_channels()
|
||||
.find(|(_, c)| c.name == channel_name)
|
||||
@@ -198,9 +198,9 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
edits
|
||||
);
|
||||
|
||||
channel_buffer.update(cx, |buffer, cx| {
|
||||
channel_buffer.update(cx, |buffer, model, cx| {
|
||||
let buffer = buffer.buffer();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
let snapshot = buffer.snapshot();
|
||||
buffer.edit(
|
||||
edits.into_iter().map(|(range, text)| {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user