History

Merge remote-tracking branch 'origin/main' into cache

# Conflicts: # crates/gpui/src/window.rs

Antonio Scandurra committed 2 years ago

Commit 94293b32 parents 11b433d 1c1d50173 files · +2668 −1961Browse files

Unified · Split

73 files
  1. .github/actions/check_formatting/action.yml +0 −15
  2. .github/actions/check_style/action.yml +17 −0
  3. .github/actions/run_tests/action.yml +19 −22
  4. .github/workflows/ci.yml +6 −13
  5. .github/workflows/randomized_tests.yml +27 −32
  6. .github/workflows/release_nightly.yml +6 −13
  7. .zed/settings.json +2 −1
  8. Cargo.lock +2 −68
  9. Cargo.toml +1 −1
  10. assets/keymaps/default.json +2 −1
  11. crates/call/src/call.rs +3 −2
  12. crates/call/src/room.rs +52 −57
  13. crates/client/src/client.rs +2 −5
  14. crates/client/src/telemetry.rs +37 −8
  15. crates/client/src/telemetry/event_coalescer.rs +279 −0
  16. crates/collab/Cargo.toml +1 −1
  17. crates/collab/migrations.sqlite/20221109000000_test_schema.sql +1 −1
  18. crates/collab/migrations/20240111085546_fix_column_name.sql +1 −0
  19. crates/collab/src/db/queries/channels.rs +2 −2
  20. crates/collab/src/db/queries/rooms.rs +7 −7
  21. crates/collab/src/db/tables/room.rs +1 −1
  22. crates/collab/src/tests/channel_buffer_tests.rs +0 −146
  23. crates/collab/src/tests/channel_guest_tests.rs +4 −6
  24. crates/collab/src/tests/channel_message_tests.rs +134 −132
  25. crates/collab/src/tests/following_tests.rs +223 −6
  26. crates/collab/src/tests/test_server.rs +62 −2
  27. crates/collab_ui/src/collab_ui.rs +0 −1
  28. crates/editor/src/display_map/wrap_map.rs +24 −30
  29. crates/editor/src/editor.rs +4 −2
  30. crates/editor/src/element.rs +38 −38
  31. crates/editor/src/hover_popover.rs +4 −6
  32. crates/editor/src/items.rs +3 −1
  33. crates/feedback/src/feedback_modal.rs +1 −40
  34. crates/file_finder/Cargo.toml +1 −0
  35. crates/file_finder/src/file_finder.rs +125 −2
  36. crates/gpui/src/action.rs +1 −1
  37. crates/gpui/src/keymap/matcher.rs +375 −370
  38. crates/gpui/src/platform/mac/platform.rs +47 −19
  39. crates/gpui/src/platform/mac/text_system.rs +49 −166
  40. crates/gpui/src/platform/mac/window.rs +5 −34
  41. crates/gpui/src/text_system.rs +10 −15
  42. crates/gpui/src/text_system/line_wrapper.rs +63 −68
  43. crates/gpui/src/window.rs +13 −4
  44. crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift +47 −3
  45. crates/live_kit_client/examples/test_app.rs +16 −19
  46. crates/live_kit_client/src/live_kit_client.rs +24 −0
  47. crates/live_kit_client/src/prod.rs +97 −52
  48. crates/live_kit_client/src/test.rs +65 −61
  49. crates/project/src/project.rs +43 −39
  50. crates/search/src/buffer_search.rs +0 −2
  51. crates/search/src/project_search.rs +119 −3
  52. crates/terminal_view/src/terminal_element.rs +15 −2
  53. crates/terminal_view/src/terminal_view.rs +4 −0
  54. crates/theme/src/settings.rs +25 −7
  55. crates/theme/src/styles/players.rs +1 −1
  56. crates/theme/src/themes/andromeda.rs +1 −1
  57. crates/theme/src/themes/atelier.rs +20 −20
  58. crates/theme/src/themes/ayu.rs +3 −3
  59. crates/theme/src/themes/gruvbox.rs +6 −6
  60. crates/theme/src/themes/one.rs +2 −2
  61. crates/theme/src/themes/rose_pine.rs +3 −3
  62. crates/theme/src/themes/sandcastle.rs +1 −1
  63. crates/theme/src/themes/solarized.rs +2 −2
  64. crates/theme/src/themes/summercamp.rs +1 −1
  65. crates/theme_importer/src/zed1/converter.rs +1 −1
  66. crates/util/src/util.rs +11 −0
  67. crates/vim/src/editor_events.rs +0 −1
  68. crates/workspace/src/pane.rs +40 −16
  69. crates/workspace/src/workspace.rs +427 −363
  70. crates/zed/src/zed.rs +1 −0
  71. docs/old/tools.md +1 −1
  72. rust-toolchain.toml +2 −1
  73. script/zed-local +36 −11
.github/actions/check_formatting/action.yml+0−15
@@ -1,15 +0,0 @@
1name: 'Check formatting'
2description: 'Checks code formatting use cargo fmt'
3
4runs:
5 using: "composite"
6 steps:
7 - name: Install Rust
8 shell: bash -euxo pipefail {0}
9 run: |
10 rustup set profile minimal
11 rustup update stable
12
13 - name: cargo fmt
14 shell: bash -euxo pipefail {0}
15 run: cargo fmt --all -- --check
.github/actions/check_style/action.yml+17−0
@@ -0,0 +1,17 @@
1name: "Check formatting"
2description: "Checks code formatting use cargo fmt"
3
4runs:
5 using: "composite"
6 steps:
7 - name: cargo fmt
8 shell: bash -euxo pipefail {0}
9 run: cargo fmt --all -- --check
10
11 - name: cargo clippy
12 shell: bash -euxo pipefail {0}
13 # clippy.toml is not currently supporting specifying allowed lints
14 # so specify those here, and disable the rest until Zed's workspace
15 # will have more fixes & suppression for the standard lint set
16 run: |
17 cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo
.github/actions/run_tests/action.yml+19−22
@@ -2,29 +2,26 @@ name: "Run tests" · 1 unmodified line
2description: "Runs the tests"2description: "Runs the tests"
3 3
4runs:4runs:
5 using: "composite"5 using: "composite"
6 steps:6 steps:
7 - name: Install Rust7 - name: Install Rust
8 shell: bash -euxo pipefail {0}8 shell: bash -euxo pipefail {0}
9 run: |9 run: |
10 rustup set profile minimal10 cargo install cargo-nextest
11 rustup update stable
12 rustup target add wasm32-wasi
13 cargo install cargo-nextest
14 11
15 - name: Install Node12 - name: Install Node
16 uses: actions/setup-node@v313 uses: actions/setup-node@v3
17 with:14 with:
18 node-version: "18"15 node-version: "18"
19 16
20 - name: Limit target directory size17 - name: Limit target directory size
21 shell: bash -euxo pipefail {0}18 shell: bash -euxo pipefail {0}
22 run: script/clear-target-dir-if-larger-than 10019 run: script/clear-target-dir-if-larger-than 100
23 20
24 - name: Run check21 - name: Run check
25 shell: bash -euxo pipefail {0}22 shell: bash -euxo pipefail {0}
26 run: cargo check --tests --workspace23 run: cargo check --tests --workspace
27 24
28 - name: Run tests25 - name: Run tests
29 shell: bash -euxo pipefail {0}26 shell: bash -euxo pipefail {0}
30 run: cargo nextest run --workspace --no-fail-fast27 run: cargo nextest run --workspace --no-fail-fast
.github/workflows/ci.yml+6−13
@@ -22,8 +22,8 @@ env: · 21 unmodified lines
22 RUST_BACKTRACE: 122 RUST_BACKTRACE: 1
23 23
24jobs:24jobs:
25 rustfmt:25 style:
26 name: Check formatting26 name: Check formatting and Clippy lints
27 runs-on:27 runs-on:
28 - self-hosted28 - self-hosted
29 - test29 - test
@@ -33,19 +33,20 @@ jobs: · 3 unmodified lines
33 with:33 with:
34 clean: false34 clean: false
35 submodules: "recursive"35 submodules: "recursive"
36 fetch-depth: 0
36 37
37 - name: Set up default .cargo/config.toml38 - name: Set up default .cargo/config.toml
38 run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml39 run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
39 40
40 - name: Run rustfmt41 - name: Run style checks
41 uses: ./.github/actions/check_formatting42 uses: ./.github/actions/check_style
42 43
43 tests:44 tests:
44 name: Run tests45 name: Run tests
45 runs-on:46 runs-on:
46 - self-hosted47 - self-hosted
47 - test48 - test
48 needs: rustfmt49 needs: style
49 steps:50 steps:
50 - name: Checkout repo51 - name: Checkout repo
51 uses: actions/checkout@v352 uses: actions/checkout@v3
@@ -75,14 +76,6 @@ jobs: · 23 unmodified lines
75 APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}76 APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
76 APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}77 APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
77 steps:78 steps:
78 - name: Install Rust
79 run: |
80 rustup set profile minimal
81 rustup update stable
82 rustup target add aarch64-apple-darwin
83 rustup target add x86_64-apple-darwin
84 rustup target add wasm32-wasi
85
86 - name: Install Node79 - name: Install Node
87 uses: actions/setup-node@v380 uses: actions/setup-node@v3
88 with:81 with:
.github/workflows/randomized_tests.yml+27−32
@@ -3,41 +3,36 @@ name: Randomized Tests · 2 unmodified lines
3concurrency: randomized-tests3concurrency: randomized-tests
4 4
5on:5on:
6 push:6 push:
7 branches:7 branches:
8 - randomized-tests-runner8 - randomized-tests-runner
9 # schedule:9 # schedule:
10 # - cron: '0 * * * *'10 # - cron: '0 * * * *'
11 11
12env:12env:
13 CARGO_TERM_COLOR: always13 CARGO_TERM_COLOR: always
14 CARGO_INCREMENTAL: 014 CARGO_INCREMENTAL: 0
15 RUST_BACKTRACE: 115 RUST_BACKTRACE: 1
16 ZED_SERVER_URL: https://zed.dev16 ZED_SERVER_URL: https://zed.dev
17 ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}17 ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
18 18
19jobs:19jobs:
20 tests:20 tests:
21 name: Run randomized tests21 name: Run randomized tests
22 runs-on:22 runs-on:
23 - self-hosted23 - self-hosted
24 - randomized-tests24 - randomized-tests
25 steps:25 steps:
26 - name: Install Rust26 - name: Install Node
27 run: |27 uses: actions/setup-node@v3
28 rustup set profile minimal28 with:
29 rustup update stable29 node-version: "18"
30 30
31 - name: Install Node31 - name: Checkout repo
32 uses: actions/setup-node@v332 uses: actions/checkout@v3
33 with:33 with:
34 node-version: '18'34 clean: false
35 submodules: "recursive"
35 36
36 - name: Checkout repo37 - name: Run randomized tests
37 uses: actions/checkout@v338 run: script/randomized-test-ci
38 with:
39 clean: false
40 submodules: 'recursive'
41
42 - name: Run randomized tests
43 run: script/randomized-test-ci
.github/workflows/release_nightly.yml+6−13
@@ -14,8 +14,8 @@ env: · 13 unmodified lines
14 RUST_BACKTRACE: 114 RUST_BACKTRACE: 1
15 15
16jobs:16jobs:
17 rustfmt:17 style:
18 name: Check formatting18 name: Check formatting and Clippy lints
19 runs-on:19 runs-on:
20 - self-hosted20 - self-hosted
21 - test21 - test
@@ -25,16 +25,17 @@ jobs: · 3 unmodified lines
25 with:25 with:
26 clean: false26 clean: false
27 submodules: "recursive"27 submodules: "recursive"
28 fetch-depth: 0
28 29
29 - name: Run rustfmt30 - name: Run style checks
30 uses: ./.github/actions/check_formatting31 uses: ./.github/actions/check_style
31 32
32 tests:33 tests:
33 name: Run tests34 name: Run tests
34 runs-on:35 runs-on:
35 - self-hosted36 - self-hosted
36 - test37 - test
37 needs: rustfmt38 needs: style
38 steps:39 steps:
39 - name: Checkout repo40 - name: Checkout repo
40 uses: actions/checkout@v341 uses: actions/checkout@v3
@@ -59,14 +60,6 @@ jobs: · 18 unmodified lines
59 DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}60 DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
60 DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}61 DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
61 steps:62 steps:
62 - name: Install Rust
63 run: |
64 rustup set profile minimal
65 rustup update stable
66 rustup target add aarch64-apple-darwin
67 rustup target add x86_64-apple-darwin
68 rustup target add wasm32-wasi
69
70 - name: Install Node63 - name: Install Node
71 uses: actions/setup-node@v364 uses: actions/setup-node@v3
72 with:65 with:
.zed/settings.json+2−1
@@ -1,5 +1,6 @@
1{1{
2 "JSON": {2 "JSON": {
3 "tab_size": 43 "tab_size": 4
4 }4 },
5 "formatter": "auto"
5}6}
Cargo.lock+2−68
@@ -1452,7 +1452,7 @@ dependencies = [ · 1451 unmodified lines
1452 1452
1453[[package]]1453[[package]]
1454name = "collab"1454name = "collab"
1455version = "0.36.0"1455version = "0.36.1"
1456dependencies = [1456dependencies = [
1457 "anyhow",1457 "anyhow",
1458 "async-trait",1458 "async-trait",
@@ -2522,6 +2522,7 @@ dependencies = [ · 1063 unmodified lines
2522name = "file_finder"2522name = "file_finder"
2523version = "0.1.0"2523version = "0.1.0"
2524dependencies = [2524dependencies = [
2525 "anyhow",
2525 "collections",2526 "collections",
2526 "ctor",2527 "ctor",
2527 "editor",2528 "editor",
@@ -3440,40 +3441,6 @@ dependencies = [ · 912 unmodified lines
3440 "tiff",3441 "tiff",
3441]3442]
3442 3443
3443[[package]]
3444name = "include-flate"
3445version = "0.2.0"
3446source = "registry+https://github.com/rust-lang/crates.io-index"
3447checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0"
3448dependencies = [
3449 "include-flate-codegen-exports",
3450 "lazy_static",
3451 "libflate",
3452]
3453
3454[[package]]
3455name = "include-flate-codegen"
3456version = "0.1.4"
3457source = "registry+https://github.com/rust-lang/crates.io-index"
3458checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8"
3459dependencies = [
3460 "libflate",
3461 "proc-macro-hack",
3462 "proc-macro2",
3463 "quote",
3464 "syn 1.0.109",
3465]
3466
3467[[package]]
3468name = "include-flate-codegen-exports"
3469version = "0.1.4"
3470source = "registry+https://github.com/rust-lang/crates.io-index"
3471checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03"
3472dependencies = [
3473 "include-flate-codegen",
3474 "proc-macro-hack",
3475]
3476
3477[[package]]3444[[package]]
3478name = "indexmap"3445name = "indexmap"
3479version = "1.9.3"3446version = "1.9.3"
@@ -3865,26 +3832,6 @@ version = "0.2.148" · 385 unmodified lines
3865source = "registry+https://github.com/rust-lang/crates.io-index"3832source = "registry+https://github.com/rust-lang/crates.io-index"
3866checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"3833checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
3867 3834
3868[[package]]
3869name = "libflate"
3870version = "1.4.0"
3871source = "registry+https://github.com/rust-lang/crates.io-index"
3872checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
3873dependencies = [
3874 "adler32",
3875 "crc32fast",
3876 "libflate_lz77",
3877]
3878
3879[[package]]
3880name = "libflate_lz77"
3881version = "1.2.0"
3882source = "registry+https://github.com/rust-lang/crates.io-index"
3883checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
3884dependencies = [
3885 "rle-decode-fast",
3886]
3887
3888[[package]]3835[[package]]
3889name = "libgit2-sys"3836name = "libgit2-sys"
3890version = "0.14.2+1.5.1"3837version = "0.14.2+1.5.1"
@@ -5462,12 +5409,6 @@ dependencies = [ · 1571 unmodified lines
5462 "version_check",5409 "version_check",
5463]5410]
5464 5411
5465[[package]]
5466name = "proc-macro-hack"
5467version = "0.5.20+deprecated"
5468source = "registry+https://github.com/rust-lang/crates.io-index"
5469checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
5470
5471[[package]]5412[[package]]
5472name = "proc-macro2"5413name = "proc-macro2"
5473version = "1.0.67"5414version = "1.0.67"
@@ -6162,12 +6103,6 @@ dependencies = [ · 688 unmodified lines
6162 "syn 1.0.109",6103 "syn 1.0.109",
6163]6104]
6164 6105
6165[[package]]
6166name = "rle-decode-fast"
6167version = "1.0.3"
6168source = "registry+https://github.com/rust-lang/crates.io-index"
6169checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
6170
6171[[package]]6106[[package]]
6172name = "rmp"6107name = "rmp"
6173version = "0.8.12"6108version = "0.8.12"
@@ -6315,7 +6250,6 @@ version = "8.0.0" · 141 unmodified lines
6315source = "registry+https://github.com/rust-lang/crates.io-index"6250source = "registry+https://github.com/rust-lang/crates.io-index"
6316checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"6251checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
6317dependencies = [6252dependencies = [
6318 "include-flate",
6319 "rust-embed-impl",6253 "rust-embed-impl",
6320 "rust-embed-utils",6254 "rust-embed-utils",
6321 "walkdir",6255 "walkdir",
Cargo.toml+1−1
@@ -110,7 +110,7 @@ prost = { version = "0.8" } · 109 unmodified lines
110rand = { version = "0.8.5" }110rand = { version = "0.8.5" }
111refineable = { path = "./crates/refineable" }111refineable = { path = "./crates/refineable" }
112regex = { version = "1.5" }112regex = { version = "1.5" }
113rust-embed = { version = "8.0", features = ["include-exclude", "compression"] }113rust-embed = { version = "8.0", features = ["include-exclude"] }
114rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }114rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
115schemars = { version = "0.8" }115schemars = { version = "0.8" }
116serde = { version = "1.0", features = ["derive", "rc"] }116serde = { version = "1.0", features = ["derive", "rc"] }
assets/keymaps/default.json+2−1
@@ -412,7 +412,8 @@ · 411 unmodified lines
412 "cmd-shift-e": "project_panel::ToggleFocus",412 "cmd-shift-e": "project_panel::ToggleFocus",
413 "cmd-?": "assistant::ToggleFocus",413 "cmd-?": "assistant::ToggleFocus",
414 "cmd-alt-s": "workspace::SaveAll",414 "cmd-alt-s": "workspace::SaveAll",
415 "cmd-k m": "language_selector::Toggle"415 "cmd-k m": "language_selector::Toggle",
416 "escape": "workspace::Unfollow"
416 }417 }
417 },418 },
418 // Bindings from Sublime Text419 // Bindings from Sublime Text
crates/call/src/call.rs+3−2
@@ -239,7 +239,8 @@ impl ActiveCall { · 238 unmodified lines
239 if result.is_ok() {239 if result.is_ok() {
240 this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;240 this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
241 } else {241 } else {
242 // TODO: Resport collaboration error242 //TODO: report collaboration error
243 log::error!("invite failed: {:?}", result);
243 }244 }
244 245
245 this.update(&mut cx, |this, cx| {246 this.update(&mut cx, |this, cx| {
@@ -282,7 +283,7 @@ impl ActiveCall { · 36 unmodified lines
282 return Task::ready(Err(anyhow!("cannot join while on another call")));283 return Task::ready(Err(anyhow!("cannot join while on another call")));
283 }284 }
284 285
285 let call = if let Some(call) = self.incoming_call.1.borrow().clone() {286 let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
286 call287 call
287 } else {288 } else {
288 return Task::ready(Err(anyhow!("no incoming call")));289 return Task::ready(Err(anyhow!("no incoming call")));
crates/call/src/room.rs+52−57
@@ -15,10 +15,7 @@ use gpui::{ · 14 unmodified lines
15 AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,15 AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
16};16};
17use language::LanguageRegistry;17use language::LanguageRegistry;
18use live_kit_client::{18use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
19 LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
20 RemoteVideoTrackUpdate,
21};
22use postage::{sink::Sink, stream::Stream, watch};19use postage::{sink::Sink, stream::Stream, watch};
23use project::Project;20use project::Project;
24use settings::Settings as _;21use settings::Settings as _;
@@ -131,30 +128,11 @@ impl Room { · 106 unmodified lines
131 }128 }
132 });129 });
133 130
134 let _maintain_video_tracks = cx.spawn({131 let _handle_updates = cx.spawn({
135 let room = room.clone();132 let room = room.clone();
136 move |this, mut cx| async move {133 move |this, mut cx| async move {
137 let mut track_video_changes = room.remote_video_track_updates();134 let mut updates = room.updates();
138 while let Some(track_change) = track_video_changes.next().await {135 while let Some(update) = updates.next().await {
139 let this = if let Some(this) = this.upgrade() {
140 this
141 } else {
142 break;
143 };
144
145 this.update(&mut cx, |this, cx| {
146 this.remote_video_track_updated(track_change, cx).log_err()
147 })
148 .ok();
149 }
150 }
151 });
152
153 let _maintain_audio_tracks = cx.spawn({
154 let room = room.clone();
155 |this, mut cx| async move {
156 let mut track_audio_changes = room.remote_audio_track_updates();
157 while let Some(track_change) = track_audio_changes.next().await {
158 let this = if let Some(this) = this.upgrade() {136 let this = if let Some(this) = this.upgrade() {
159 this137 this
160 } else {138 } else {
@@ -162,7 +140,7 @@ impl Room { · 1 unmodified line
162 };140 };
163 141
164 this.update(&mut cx, |this, cx| {142 this.update(&mut cx, |this, cx| {
165 this.remote_audio_track_updated(track_change, cx).log_err()143 this.live_kit_room_updated(update, cx).log_err()
166 })144 })
167 .ok();145 .ok();
168 }146 }
@@ -195,7 +173,7 @@ impl Room { · 26 unmodified lines
195 deafened: false,173 deafened: false,
196 speaking: false,174 speaking: false,
197 _maintain_room,175 _maintain_room,
198 _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],176 _handle_updates,
199 })177 })
200 } else {178 } else {
201 None179 None
@@ -877,8 +855,8 @@ impl Room { · 675 unmodified lines
877 .remote_audio_track_publications(&user.id.to_string());855 .remote_audio_track_publications(&user.id.to_string());
878 856
879 for track in video_tracks {857 for track in video_tracks {
880 this.remote_video_track_updated(858 this.live_kit_room_updated(
881 RemoteVideoTrackUpdate::Subscribed(track),859 RoomUpdate::SubscribedToRemoteVideoTrack(track),
882 cx,860 cx,
883 )861 )
884 .log_err();862 .log_err();
@@ -887,8 +865,8 @@ impl Room { · 2 unmodified lines
887 for (track, publication) in865 for (track, publication) in
888 audio_tracks.iter().zip(publications.iter())866 audio_tracks.iter().zip(publications.iter())
889 {867 {
890 this.remote_audio_track_updated(868 this.live_kit_room_updated(
891 RemoteAudioTrackUpdate::Subscribed(869 RoomUpdate::SubscribedToRemoteAudioTrack(
892 track.clone(),870 track.clone(),
893 publication.clone(),871 publication.clone(),
894 ),872 ),
@@ -979,13 +957,13 @@ impl Room { · 84 unmodified lines
979 }957 }
980 }958 }
981 959
982 fn remote_video_track_updated(960 fn live_kit_room_updated(
983 &mut self,961 &mut self,
984 change: RemoteVideoTrackUpdate,962 update: RoomUpdate,
985 cx: &mut ModelContext<Self>,963 cx: &mut ModelContext<Self>,
986 ) -> Result<()> {964 ) -> Result<()> {
987 match change {965 match update {
988 RemoteVideoTrackUpdate::Subscribed(track) => {966 RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
989 let user_id = track.publisher_id().parse()?;967 let user_id = track.publisher_id().parse()?;
990 let track_id = track.sid().to_string();968 let track_id = track.sid().to_string();
991 let participant = self969 let participant = self
@@ -997,7 +975,8 @@ impl Room { · 5 unmodified lines
997 participant_id: participant.peer_id,975 participant_id: participant.peer_id,
998 });976 });
999 }977 }
1000 RemoteVideoTrackUpdate::Unsubscribed {978
979 RoomUpdate::UnsubscribedFromRemoteVideoTrack {
1001 publisher_id,980 publisher_id,
1002 track_id,981 track_id,
1003 } => {982 } => {
@@ -1011,19 +990,8 @@ impl Room { · 7 unmodified lines
1011 participant_id: participant.peer_id,990 participant_id: participant.peer_id,
1012 });991 });
1013 }992 }
1014 }
1015 993
1016 cx.notify();994 RoomUpdate::ActiveSpeakersChanged { speakers } => {
1017 Ok(())
1018 }
1019
1020 fn remote_audio_track_updated(
1021 &mut self,
1022 change: RemoteAudioTrackUpdate,
1023 cx: &mut ModelContext<Self>,
1024 ) -> Result<()> {
1025 match change {
1026 RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => {
1027 let mut speaker_ids = speakers995 let mut speaker_ids = speakers
1028 .into_iter()996 .into_iter()
1029 .filter_map(|speaker_sid| speaker_sid.parse().ok())997 .filter_map(|speaker_sid| speaker_sid.parse().ok())
@@ -1045,9 +1013,9 @@ impl Room { · 15 unmodified lines
1045 }1013 }
1046 }1014 }
1047 }1015 }
1048 cx.notify();
1049 }1016 }
1050 RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {1017
1018 RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } => {
1051 let mut found = false;1019 let mut found = false;
1052 for participant in &mut self.remote_participants.values_mut() {1020 for participant in &mut self.remote_participants.values_mut() {
1053 for track in participant.audio_tracks.values() {1021 for track in participant.audio_tracks.values() {
@@ -1061,10 +1029,9 @@ impl Room { · 7 unmodified lines
1061 break;1029 break;
1062 }1030 }
1063 }1031 }
1064
1065 cx.notify();
1066 }1032 }
1067 RemoteAudioTrackUpdate::Subscribed(track, publication) => {1033
1034 RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
1068 let user_id = track.publisher_id().parse()?;1035 let user_id = track.publisher_id().parse()?;
1069 let track_id = track.sid().to_string();1036 let track_id = track.sid().to_string();
1070 let participant = self1037 let participant = self
@@ -1078,7 +1045,8 @@ impl Room { · 7 unmodified lines
1078 participant_id: participant.peer_id,1045 participant_id: participant.peer_id,
1079 });1046 });
1080 }1047 }
1081 RemoteAudioTrackUpdate::Unsubscribed {1048
1049 RoomUpdate::UnsubscribedFromRemoteAudioTrack {
1082 publisher_id,1050 publisher_id,
1083 track_id,1051 track_id,
1084 } => {1052 } => {
@@ -1092,6 +1060,28 @@ impl Room { · 7 unmodified lines
1092 participant_id: participant.peer_id,1060 participant_id: participant.peer_id,
1093 });1061 });
1094 }1062 }
1063
1064 RoomUpdate::LocalAudioTrackUnpublished { publication } => {
1065 log::info!("unpublished audio track {}", publication.sid());
1066 if let Some(room) = &mut self.live_kit {
1067 room.microphone_track = LocalTrack::None;
1068 }
1069 }
1070
1071 RoomUpdate::LocalVideoTrackUnpublished { publication } => {
1072 log::info!("unpublished video track {}", publication.sid());
1073 if let Some(room) = &mut self.live_kit {
1074 room.screen_track = LocalTrack::None;
1075 }
1076 }
1077
1078 RoomUpdate::LocalAudioTrackPublished { publication } => {
1079 log::info!("published audio track {}", publication.sid());
1080 }
1081
1082 RoomUpdate::LocalVideoTrackPublished { publication } => {
1083 log::info!("published video track {}", publication.sid());
1084 }
1095 }1085 }
1096 1086
1097 cx.notify();1087 cx.notify();
@@ -1235,7 +1225,12 @@ impl Room { · 137 unmodified lines
1235 };1225 };
1236 1226
1237 self.client.send(proto::UnshareProject { project_id })?;1227 self.client.send(proto::UnshareProject { project_id })?;
1238 project.update(cx, |this, cx| this.unshare(cx))1228 project.update(cx, |this, cx| this.unshare(cx))?;
1229
1230 if self.local_participant.active_project == Some(project.downgrade()) {
1231 self.set_location(Some(&project), cx).detach_and_log_err(cx);
1232 }
1233 Ok(())
1239 }1234 }
1240 1235
1241 pub(crate) fn set_location(1236 pub(crate) fn set_location(
@@ -1597,7 +1592,7 @@ struct LiveKitRoom { · 355 unmodified lines
1597 speaking: bool,1592 speaking: bool,
1598 next_publish_id: usize,1593 next_publish_id: usize,
1599 _maintain_room: Task<()>,1594 _maintain_room: Task<()>,
1600 _maintain_tracks: [Task<()>; 2],1595 _handle_updates: Task<()>,
1601}1596}
1602 1597
1603impl LiveKitRoom {1598impl LiveKitRoom {
crates/client/src/client.rs+2−5
@@ -1371,10 +1371,7 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> { · 1370 unmodified lines
1371 })1371 })
1372}1372}
1373 1373
1374async fn write_credentials_to_keychain(1374fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext) -> Result<()> {
1375 credentials: Credentials,
1376 cx: &AsyncAppContext,
1377) -> Result<()> {
1378 cx.update(move |cx| {1375 cx.update(move |cx| {
1379 cx.write_credentials(1376 cx.write_credentials(
1380 &ZED_SERVER_URL,1377 &ZED_SERVER_URL,
@@ -1384,7 +1381,7 @@ async fn write_credentials_to_keychain( · 3 unmodified lines
1384 })?1381 })?
1385}1382}
1386 1383
1387async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {1384fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
1388 cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?1385 cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
1389}1386}
1390 1387
crates/client/src/telemetry.rs+37−8
@@ -1,3 +1,5 @@
1mod event_coalescer;
2
1use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};3use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
2use chrono::{DateTime, Utc};4use chrono::{DateTime, Utc};
3use futures::Future;5use futures::Future;
@@ -5,7 +7,6 @@ use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; · 1 unmodified line
5use lazy_static::lazy_static;7use lazy_static::lazy_static;
6use parking_lot::Mutex;8use parking_lot::Mutex;
7use serde::Serialize;9use serde::Serialize;
8use serde_json;
9use settings::{Settings, SettingsStore};10use settings::{Settings, SettingsStore};
10use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};11use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
11use sysinfo::{12use sysinfo::{
@@ -15,6 +16,8 @@ use tempfile::NamedTempFile; · 3 unmodified lines
15use util::http::HttpClient;16use util::http::HttpClient;
16use util::{channel::ReleaseChannel, TryFutureExt};17use util::{channel::ReleaseChannel, TryFutureExt};
17 18
19use self::event_coalescer::EventCoalescer;
20
18pub struct Telemetry {21pub struct Telemetry {
19 http_client: Arc<dyn HttpClient>,22 http_client: Arc<dyn HttpClient>,
20 executor: BackgroundExecutor,23 executor: BackgroundExecutor,
@@ -34,6 +37,7 @@ struct TelemetryState { · 13 unmodified lines
34 log_file: Option<NamedTempFile>,37 log_file: Option<NamedTempFile>,
35 is_staff: Option<bool>,38 is_staff: Option<bool>,
36 first_event_datetime: Option<DateTime<Utc>>,39 first_event_datetime: Option<DateTime<Utc>>,
40 event_coalescer: EventCoalescer,
37}41}
38 42
39const EVENTS_URL_PATH: &'static str = "/api/events";43const EVENTS_URL_PATH: &'static str = "/api/events";
@@ -118,19 +122,24 @@ pub enum Event { · 78 unmodified lines
118 value: String,122 value: String,
119 milliseconds_since_first_event: i64,123 milliseconds_since_first_event: i64,
120 },124 },
125 Edit {
126 duration: i64,
127 environment: &'static str,
128 milliseconds_since_first_event: i64,
129 },
121}130}
122 131
123#[cfg(debug_assertions)]132#[cfg(debug_assertions)]
124const MAX_QUEUE_LEN: usize = 1;133const MAX_QUEUE_LEN: usize = 5;
125 134
126#[cfg(not(debug_assertions))]135#[cfg(not(debug_assertions))]
127const MAX_QUEUE_LEN: usize = 50;136const MAX_QUEUE_LEN: usize = 50;
128 137
129#[cfg(debug_assertions)]138#[cfg(debug_assertions)]
130const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);139const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
131 140
132#[cfg(not(debug_assertions))]141#[cfg(not(debug_assertions))]
133const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);142const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
134 143
135impl Telemetry {144impl Telemetry {
136 pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {145 pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
@@ -150,11 +159,12 @@ impl Telemetry { · 13 unmodified lines
150 installation_id: None,159 installation_id: None,
151 metrics_id: None,160 metrics_id: None,
152 session_id: None,161 session_id: None,
153 events_queue: Default::default(),162 events_queue: Vec::new(),
154 flush_events_task: Default::default(),163 flush_events_task: None,
155 log_file: None,164 log_file: None,
156 is_staff: None,165 is_staff: None,
157 first_event_datetime: None,166 first_event_datetime: None,
167 event_coalescer: EventCoalescer::new(),
158 }));168 }));
159 169
160 cx.observe_global::<SettingsStore>({170 cx.observe_global::<SettingsStore>({
@@ -194,7 +204,7 @@ impl Telemetry { · 33 unmodified lines
194 #[cfg(not(any(test, feature = "test-support")))]204 #[cfg(not(any(test, feature = "test-support")))]
195 fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {205 fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
196 self.report_app_event("close");206 self.report_app_event("close");
197 self.flush_events();207 // TODO: close final edit period and make sure it's sent
198 Task::ready(())208 Task::ready(())
199 }209 }
200 210
@@ -392,6 +402,22 @@ impl Telemetry { · 191 unmodified lines
392 }402 }
393 }403 }
394 404
405 pub fn log_edit_event(self: &Arc<Self>, environment: &'static str) {
406 let mut state = self.state.lock();
407 let period_data = state.event_coalescer.log_event(environment);
408 drop(state);
409
410 if let Some((start, end, environment)) = period_data {
411 let event = Event::Edit {
412 duration: end.timestamp_millis() - start.timestamp_millis(),
413 environment,
414 milliseconds_since_first_event: self.milliseconds_since_first_event(),
415 };
416
417 self.report_event(event);
418 }
419 }
420
395 fn report_event(self: &Arc<Self>, event: Event) {421 fn report_event(self: &Arc<Self>, event: Event) {
396 let mut state = self.state.lock();422 let mut state = self.state.lock();
397 423
@@ -410,7 +436,7 @@ impl Telemetry { · 12 unmodified lines
410 let this = self.clone();436 let this = self.clone();
411 let executor = self.executor.clone();437 let executor = self.executor.clone();
412 state.flush_events_task = Some(self.executor.spawn(async move {438 state.flush_events_task = Some(self.executor.spawn(async move {
413 executor.timer(DEBOUNCE_INTERVAL).await;439 executor.timer(FLUSH_DEBOUNCE_INTERVAL).await;
414 this.flush_events();440 this.flush_events();
415 }));441 }));
416 }442 }
@@ -435,6 +461,9 @@ impl Telemetry { · 18 unmodified lines
435 let mut events = mem::take(&mut state.events_queue);461 let mut events = mem::take(&mut state.events_queue);
436 state.flush_events_task.take();462 state.flush_events_task.take();
437 drop(state);463 drop(state);
464 if events.is_empty() {
465 return;
466 }
438 467
439 let this = self.clone();468 let this = self.clone();
440 self.executor469 self.executor
crates/client/src/telemetry/event_coalescer.rs+279−0
@@ -0,0 +1,279 @@
1use chrono::{DateTime, Duration, Utc};
2use std::time;
3
4const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
5const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1);
6
7#[derive(Debug, PartialEq)]
8struct PeriodData {
9 environment: &'static str,
10 start: DateTime<Utc>,
11 end: Option<DateTime<Utc>>,
12}
13
14pub struct EventCoalescer {
15 state: Option<PeriodData>,
16}
17
18impl EventCoalescer {
19 pub fn new() -> Self {
20 Self { state: None }
21 }
22
23 pub fn log_event(
24 &mut self,
25 environment: &'static str,
26 ) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
27 self.log_event_with_time(Utc::now(), environment)
28 }
29
30 // pub fn close_current_period(&mut self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
31 // self.environment.map(|env| self.log_event(env)).flatten()
32 // }
33
34 fn log_event_with_time(
35 &mut self,
36 log_time: DateTime<Utc>,
37 environment: &'static str,
38 ) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
39 let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
40
41 let Some(state) = &mut self.state else {
42 self.state = Some(PeriodData {
43 start: log_time,
44 end: None,
45 environment,
46 });
47 return None;
48 };
49
50 let period_end = state
51 .end
52 .unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT);
53 let within_timeout = log_time - period_end < coalesce_timeout;
54 let environment_is_same = state.environment == environment;
55 let should_coaelesce = !within_timeout || !environment_is_same;
56
57 if should_coaelesce {
58 let previous_environment = state.environment;
59 let original_start = state.start;
60
61 state.start = log_time;
62 state.end = None;
63 state.environment = environment;
64
65 return Some((
66 original_start,
67 if within_timeout { log_time } else { period_end },
68 previous_environment,
69 ));
70 }
71
72 state.end = Some(log_time);
73
74 None
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use chrono::TimeZone;
81
82 use super::*;
83
84 #[test]
85 fn test_same_context_exceeding_timeout() {
86 let environment_1 = "environment_1";
87 let mut event_coalescer = EventCoalescer::new();
88
89 assert_eq!(event_coalescer.state, None);
90
91 let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
92 let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
93
94 assert_eq!(period_data, None);
95 assert_eq!(
96 event_coalescer.state,
97 Some(PeriodData {
98 start: period_start,
99 end: None,
100 environment: environment_1,
101 })
102 );
103
104 let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
105 let mut period_end = period_start;
106
107 // Ensure that many calls within the timeout don't start a new period
108 for _ in 0..100 {
109 period_end += within_timeout_adjustment;
110 let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
111
112 assert_eq!(period_data, None);
113 assert_eq!(
114 event_coalescer.state,
115 Some(PeriodData {
116 start: period_start,
117 end: Some(period_end),
118 environment: environment_1,
119 })
120 );
121 }
122
123 let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
124 // Logging an event exceeding the timeout should start a new period
125 let new_period_start = period_end + exceed_timeout_adjustment;
126 let period_data = event_coalescer.log_event_with_time(new_period_start, environment_1);
127
128 assert_eq!(period_data, Some((period_start, period_end, environment_1)));
129 assert_eq!(
130 event_coalescer.state,
131 Some(PeriodData {
132 start: new_period_start,
133 end: None,
134 environment: environment_1,
135 })
136 );
137 }
138
139 #[test]
140 fn test_different_environment_under_timeout() {
141 let environment_1 = "environment_1";
142 let mut event_coalescer = EventCoalescer::new();
143
144 assert_eq!(event_coalescer.state, None);
145
146 let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
147 let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
148
149 assert_eq!(period_data, None);
150 assert_eq!(
151 event_coalescer.state,
152 Some(PeriodData {
153 start: period_start,
154 end: None,
155 environment: environment_1,
156 })
157 );
158
159 let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
160 let period_end = period_start + within_timeout_adjustment;
161 let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
162
163 assert_eq!(period_data, None);
164 assert_eq!(
165 event_coalescer.state,
166 Some(PeriodData {
167 start: period_start,
168 end: Some(period_end),
169 environment: environment_1,
170 })
171 );
172
173 // Logging an event within the timeout but with a different environment should start a new period
174 let period_end = period_end + within_timeout_adjustment;
175 let environment_2 = "environment_2";
176 let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
177
178 assert_eq!(period_data, Some((period_start, period_end, environment_1)));
179 assert_eq!(
180 event_coalescer.state,
181 Some(PeriodData {
182 start: period_end,
183 end: None,
184 environment: environment_2,
185 })
186 );
187 }
188
189 #[test]
190 fn test_switching_environment_while_within_timeout() {
191 let environment_1 = "environment_1";
192 let mut event_coalescer = EventCoalescer::new();
193
194 assert_eq!(event_coalescer.state, None);
195
196 let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
197 let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
198
199 assert_eq!(period_data, None);
200 assert_eq!(
201 event_coalescer.state,
202 Some(PeriodData {
203 start: period_start,
204 end: None,
205 environment: environment_1,
206 })
207 );
208
209 let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
210 let period_end = period_start + within_timeout_adjustment;
211 let environment_2 = "environment_2";
212 let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
213
214 assert_eq!(period_data, Some((period_start, period_end, environment_1)));
215 assert_eq!(
216 event_coalescer.state,
217 Some(PeriodData {
218 start: period_end,
219 end: None,
220 environment: environment_2,
221 })
222 );
223 }
224 // // 0 20 40 60
225 // // |-------------------|-------------------|-------------------|-------------------
226 // // |--------|----------env change
227 // // |-------------------
228 // // |period_start |period_end
229 // // |new_period_start
230
231 #[test]
232 fn test_switching_environment_while_exceeding_timeout() {
233 let environment_1 = "environment_1";
234 let mut event_coalescer = EventCoalescer::new();
235
236 assert_eq!(event_coalescer.state, None);
237
238 let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
239 let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
240
241 assert_eq!(period_data, None);
242 assert_eq!(
243 event_coalescer.state,
244 Some(PeriodData {
245 start: period_start,
246 end: None,
247 environment: environment_1,
248 })
249 );
250
251 let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
252 let period_end = period_start + exceed_timeout_adjustment;
253 let environment_2 = "environment_2";
254 let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
255
256 assert_eq!(
257 period_data,
258 Some((
259 period_start,
260 period_start + SIMULATED_DURATION_FOR_SINGLE_EVENT,
261 environment_1
262 ))
263 );
264 assert_eq!(
265 event_coalescer.state,
266 Some(PeriodData {
267 start: period_end,
268 end: None,
269 environment: environment_2,
270 })
271 );
272 }
273 // 0 20 40 60
274 // |-------------------|-------------------|-------------------|-------------------
275 // |--------|----------------------------------------env change
276 // |-------------------|
277 // |period_start |period_end
278 // |new_period_start
279}
crates/collab/Cargo.toml+1−1
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"] · 2 unmodified lines
3default-run = "collab"3default-run = "collab"
4edition = "2021"4edition = "2021"
5name = "collab"5name = "collab"
6version = "0.36.0"6version = "0.36.1"
7publish = false7publish = false
8 8
9[[bin]]9[[bin]]
crates/collab/migrations.sqlite/20221109000000_test_schema.sql+1−1
@@ -37,7 +37,7 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); · 36 unmodified lines
37CREATE TABLE "rooms" (37CREATE TABLE "rooms" (
38 "id" INTEGER PRIMARY KEY AUTOINCREMENT,38 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
39 "live_kit_room" VARCHAR NOT NULL,39 "live_kit_room" VARCHAR NOT NULL,
40 "enviroment" VARCHAR,40 "environment" VARCHAR,
41 "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE41 "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
42);42);
43CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");43CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
crates/collab/migrations/20240111085546_fix_column_name.sql+1−0
@@ -0,0 +1 @@
1ALTER TABLE rooms ADD COLUMN environment TEXT;
crates/collab/src/db/queries/channels.rs+2−2
@@ -1180,7 +1180,7 @@ impl Database { · 1179 unmodified lines
1180 .await?;1180 .await?;
1181 1181
1182 let room_id = if let Some(room) = room {1182 let room_id = if let Some(room) = room {
1183 if let Some(env) = room.enviroment {1183 if let Some(env) = room.environment {
1184 if &env != environment {1184 if &env != environment {
1185 Err(anyhow!("must join using the {} release", env))?;1185 Err(anyhow!("must join using the {} release", env))?;
1186 }1186 }
@@ -1190,7 +1190,7 @@ impl Database { · 3 unmodified lines
1190 let result = room::Entity::insert(room::ActiveModel {1190 let result = room::Entity::insert(room::ActiveModel {
1191 channel_id: ActiveValue::Set(Some(channel_id)),1191 channel_id: ActiveValue::Set(Some(channel_id)),
1192 live_kit_room: ActiveValue::Set(live_kit_room.to_string()),1192 live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
1193 enviroment: ActiveValue::Set(Some(environment.to_string())),1193 environment: ActiveValue::Set(Some(environment.to_string())),
1194 ..Default::default()1194 ..Default::default()
1195 })1195 })
1196 .exec(&*tx)1196 .exec(&*tx)
crates/collab/src/db/queries/rooms.rs+7−7
@@ -112,7 +112,7 @@ impl Database { · 111 unmodified lines
112 self.transaction(|tx| async move {112 self.transaction(|tx| async move {
113 let room = room::ActiveModel {113 let room = room::ActiveModel {
114 live_kit_room: ActiveValue::set(live_kit_room.into()),114 live_kit_room: ActiveValue::set(live_kit_room.into()),
115 enviroment: ActiveValue::set(Some(release_channel.to_string())),115 environment: ActiveValue::set(Some(release_channel.to_string())),
116 ..Default::default()116 ..Default::default()
117 }117 }
118 .insert(&*tx)118 .insert(&*tx)
@@ -299,28 +299,28 @@ impl Database { · 180 unmodified lines
299 room_id: RoomId,299 room_id: RoomId,
300 user_id: UserId,300 user_id: UserId,
301 connection: ConnectionId,301 connection: ConnectionId,
302 enviroment: &str,302 environment: &str,
303 ) -> Result<RoomGuard<JoinRoom>> {303 ) -> Result<RoomGuard<JoinRoom>> {
304 self.room_transaction(room_id, |tx| async move {304 self.room_transaction(room_id, |tx| async move {
305 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]305 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
306 enum QueryChannelIdAndEnviroment {306 enum QueryChannelIdAndEnvironment {
307 ChannelId,307 ChannelId,
308 Enviroment,308 Environment,
309 }309 }
310 310
311 let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =311 let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
312 room::Entity::find()312 room::Entity::find()
313 .select_only()313 .select_only()
314 .column(room::Column::ChannelId)314 .column(room::Column::ChannelId)
315 .column(room::Column::Enviroment)315 .column(room::Column::Environment)
316 .filter(room::Column::Id.eq(room_id))316 .filter(room::Column::Id.eq(room_id))
317 .into_values::<_, QueryChannelIdAndEnviroment>()317 .into_values::<_, QueryChannelIdAndEnvironment>()
318 .one(&*tx)318 .one(&*tx)
319 .await?319 .await?
320 .ok_or_else(|| anyhow!("no such room"))?;320 .ok_or_else(|| anyhow!("no such room"))?;
321 321
322 if let Some(release_channel) = release_channel {322 if let Some(release_channel) = release_channel {
323 if &release_channel != enviroment {323 if &release_channel != environment {
324 Err(anyhow!("must join using the {} release", release_channel))?;324 Err(anyhow!("must join using the {} release", release_channel))?;
325 }325 }
326 }326 }
crates/collab/src/db/tables/room.rs+1−1
@@ -8,7 +8,7 @@ pub struct Model { · 7 unmodified lines
8 pub id: RoomId,8 pub id: RoomId,
9 pub live_kit_room: String,9 pub live_kit_room: String,
10 pub channel_id: Option<ChannelId>,10 pub channel_id: Option<ChannelId>,
11 pub enviroment: Option<String>,11 pub environment: Option<String>,
12}12}
13 13
14#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]14#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
crates/collab/src/tests/channel_buffer_tests.rs+0−146
@@ -599,152 +599,6 @@ async fn test_channel_buffers_and_server_restarts( · 598 unmodified lines
599 });599 });
600}600}
601 601
602#[gpui::test(iterations = 10)]
603async fn test_following_to_channel_notes_without_a_shared_project(
604 deterministic: BackgroundExecutor,
605 mut cx_a: &mut TestAppContext,
606 mut cx_b: &mut TestAppContext,
607 mut cx_c: &mut TestAppContext,
608) {
609 let mut server = TestServer::start(deterministic.clone()).await;
610 let client_a = server.create_client(cx_a, "user_a").await;
611 let client_b = server.create_client(cx_b, "user_b").await;
612
613 let client_c = server.create_client(cx_c, "user_c").await;
614
615 cx_a.update(editor::init);
616 cx_b.update(editor::init);
617 cx_c.update(editor::init);
618 cx_a.update(collab_ui::channel_view::init);
619 cx_b.update(collab_ui::channel_view::init);
620 cx_c.update(collab_ui::channel_view::init);
621
622 let channel_1_id = server
623 .make_channel(
624 "channel-1",
625 None,
626 (&client_a, cx_a),
627 &mut [(&client_b, cx_b), (&client_c, cx_c)],
628 )
629 .await;
630 let channel_2_id = server
631 .make_channel(
632 "channel-2",
633 None,
634 (&client_a, cx_a),
635 &mut [(&client_b, cx_b), (&client_c, cx_c)],
636 )
637 .await;
638
639 // Clients A, B, and C join a channel.
640 let active_call_a = cx_a.read(ActiveCall::global);
641 let active_call_b = cx_b.read(ActiveCall::global);
642 let active_call_c = cx_c.read(ActiveCall::global);
643 for (call, cx) in [
644 (&active_call_a, &mut cx_a),
645 (&active_call_b, &mut cx_b),
646 (&active_call_c, &mut cx_c),
647 ] {
648 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
649 .await
650 .unwrap();
651 }
652 deterministic.run_until_parked();
653
654 // Clients A, B, and C all open their own unshared projects.
655 client_a.fs().insert_tree("/a", json!({})).await;
656 client_b.fs().insert_tree("/b", json!({})).await;
657 client_c.fs().insert_tree("/c", json!({})).await;
658 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
659 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
660 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
661 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
662 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
663 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
664
665 active_call_a
666 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
667 .await
668 .unwrap();
669
670 // Client A opens the notes for channel 1.
671 let channel_view_1_a = cx_a
672 .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
673 .await
674 .unwrap();
675 channel_view_1_a.update(cx_a, |notes, cx| {
676 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
677 notes.editor.update(cx, |editor, cx| {
678 editor.insert("Hello from A.", cx);
679 editor.change_selections(None, cx, |selections| {
680 selections.select_ranges(vec![3..4]);
681 });
682 });
683 });
684
685 // Client B follows client A.
686 workspace_b
687 .update(cx_b, |workspace, cx| {
688 workspace
689 .start_following(client_a.peer_id().unwrap(), cx)
690 .unwrap()
691 })
692 .await
693 .unwrap();
694
695 // Client B is taken to the notes for channel 1, with the same
696 // text selected as client A.
697 deterministic.run_until_parked();
698 let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| {
699 assert_eq!(
700 workspace.leader_for_pane(workspace.active_pane()),
701 Some(client_a.peer_id().unwrap())
702 );
703 workspace
704 .active_item(cx)
705 .expect("no active item")
706 .downcast::<ChannelView>()
707 .expect("active item is not a channel view")
708 });
709 channel_view_1_b.update(cx_b, |notes, cx| {
710 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
711 let editor = notes.editor.read(cx);
712 assert_eq!(editor.text(cx), "Hello from A.");
713 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
714 });
715
716 // Client A opens the notes for channel 2.
717 eprintln!("opening -------------------->");
718
719 let channel_view_2_a = cx_a
720 .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
721 .await
722 .unwrap();
723 channel_view_2_a.update(cx_a, |notes, cx| {
724 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
725 });
726
727 // Client B is taken to the notes for channel 2.
728 deterministic.run_until_parked();
729
730 eprintln!("opening <--------------------");
731
732 let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| {
733 assert_eq!(
734 workspace.leader_for_pane(workspace.active_pane()),
735 Some(client_a.peer_id().unwrap())
736 );
737 workspace
738 .active_item(cx)
739 .expect("no active item")
740 .downcast::<ChannelView>()
741 .expect("active item is not a channel view")
742 });
743 channel_view_2_b.update(cx_b, |notes, cx| {
744 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
745 });
746}
747
748#[gpui::test]602#[gpui::test]
749async fn test_channel_buffer_changes(603async fn test_channel_buffer_changes(
750 deterministic: BackgroundExecutor,604 deterministic: BackgroundExecutor,
crates/collab/src/tests/channel_guest_tests.rs+4−6
@@ -104,12 +104,10 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test · 103 unmodified lines
104 });104 });
105 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));105 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
106 assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));106 assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
107 assert!(dbg!(107 assert!(room_b
108 room_b108 .update(cx_b, |room, cx| room.share_microphone(cx))
109 .update(cx_b, |room, cx| room.share_microphone(cx))109 .await
110 .await110 .is_err());
111 )
112 .is_err());
113 111
114 // B is promoted112 // B is promoted
115 active_call_a113 active_call_a
crates/collab/src/tests/channel_message_tests.rs+134−132
@@ -1,7 +1,9 @@
1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2use channel::{ChannelChat, ChannelMessageId, MessageParams};2use channel::{ChannelChat, ChannelMessageId, MessageParams};
3use collab_ui::chat_panel::ChatPanel;
3use gpui::{BackgroundExecutor, Model, TestAppContext};4use gpui::{BackgroundExecutor, Model, TestAppContext};
4use rpc::Notification;5use rpc::Notification;
6use workspace::dock::Panel;
5 7
6#[gpui::test]8#[gpui::test]
7async fn test_basic_channel_messages(9async fn test_basic_channel_messages(
@@ -273,135 +275,135 @@ fn assert_messages(chat: &Model<ChannelChat>, messages: &[&str], cx: &mut TestAp · 265 unmodified lines
273 );275 );
274}276}
275 277
276//todo!(collab_ui)278#[gpui::test]
277// #[gpui::test]279async fn test_channel_message_changes(
278// async fn test_channel_message_changes(280 executor: BackgroundExecutor,
279// executor: BackgroundExecutor,281 cx_a: &mut TestAppContext,
280// cx_a: &mut TestAppContext,282 cx_b: &mut TestAppContext,
281// cx_b: &mut TestAppContext,283) {
282// ) {284 let mut server = TestServer::start(executor.clone()).await;
283// let mut server = TestServer::start(&executor).await;285 let client_a = server.create_client(cx_a, "user_a").await;
284// let client_a = server.create_client(cx_a, "user_a").await;286 let client_b = server.create_client(cx_b, "user_b").await;
285// let client_b = server.create_client(cx_b, "user_b").await;287
286 288 let channel_id = server
287// let channel_id = server289 .make_channel(
288// .make_channel(290 "the-channel",
289// "the-channel",291 None,
290// None,292 (&client_a, cx_a),
291// (&client_a, cx_a),293 &mut [(&client_b, cx_b)],
292// &mut [(&client_b, cx_b)],294 )
293// )295 .await;
294// .await;296
295 297 // Client A sends a message, client B should see that there is a new message.
296// // Client A sends a message, client B should see that there is a new message.298 let channel_chat_a = client_a
297// let channel_chat_a = client_a299 .channel_store()
298// .channel_store()300 .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
299// .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))301 .await
300// .await302 .unwrap();
301// .unwrap();303
302 304 channel_chat_a
303// channel_chat_a305 .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
304// .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())306 .await
305// .await307 .unwrap();
306// .unwrap();308
307 309 executor.run_until_parked();
308// executor.run_until_parked();310
309 311 let b_has_messages = cx_b.update(|cx| {
310// let b_has_messages = cx_b.read_with(|cx| {312 client_b
311// client_b313 .channel_store()
312// .channel_store()314 .read(cx)
313// .read(cx)315 .has_new_messages(channel_id)
314// .has_new_messages(channel_id)316 .unwrap()
315// .unwrap()317 });
316// });318
317 319 assert!(b_has_messages);
318// assert!(b_has_messages);320
319 321 // Opening the chat should clear the changed flag.
320// // Opening the chat should clear the changed flag.322 cx_b.update(|cx| {
321// cx_b.update(|cx| {323 collab_ui::init(&client_b.app_state, cx);
322// collab_ui::init(&client_b.app_state, cx);324 });
323// });325 let project_b = client_b.build_empty_local_project(cx_b);
324// let project_b = client_b.build_empty_local_project(cx_b);326 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
325// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);327
326// let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));328 let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
327// chat_panel_b329 chat_panel_b
328// .update(cx_b, |chat_panel, cx| {330 .update(cx_b, |chat_panel, cx| {
329// chat_panel.set_active(true, cx);331 chat_panel.set_active(true, cx);
330// chat_panel.select_channel(channel_id, None, cx)332 chat_panel.select_channel(channel_id, None, cx)
331// })333 })
332// .await334 .await
333// .unwrap();335 .unwrap();
334 336
335// executor.run_until_parked();337 executor.run_until_parked();
336 338
337// let b_has_messages = cx_b.read_with(|cx| {339 let b_has_messages = cx_b.update(|cx| {
338// client_b340 client_b
339// .channel_store()341 .channel_store()
340// .read(cx)342 .read(cx)
341// .has_new_messages(channel_id)343 .has_new_messages(channel_id)
342// .unwrap()344 .unwrap()
343// });345 });
344 346
345// assert!(!b_has_messages);347 assert!(!b_has_messages);
346 348
347// // Sending a message while the chat is open should not change the flag.349 // Sending a message while the chat is open should not change the flag.
348// channel_chat_a350 channel_chat_a
349// .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())351 .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
350// .await352 .await
351// .unwrap();353 .unwrap();
352 354
353// executor.run_until_parked();355 executor.run_until_parked();
354 356
355// let b_has_messages = cx_b.read_with(|cx| {357 let b_has_messages = cx_b.update(|cx| {
356// client_b358 client_b
357// .channel_store()359 .channel_store()
358// .read(cx)360 .read(cx)
359// .has_new_messages(channel_id)361 .has_new_messages(channel_id)
360// .unwrap()362 .unwrap()
361// });363 });
362 364
363// assert!(!b_has_messages);365 assert!(!b_has_messages);
364 366
365// // Sending a message while the chat is closed should change the flag.367 // Sending a message while the chat is closed should change the flag.
366// chat_panel_b.update(cx_b, |chat_panel, cx| {368 chat_panel_b.update(cx_b, |chat_panel, cx| {
367// chat_panel.set_active(false, cx);369 chat_panel.set_active(false, cx);
368// });370 });
369 371
370// // Sending a message while the chat is open should not change the flag.372 // Sending a message while the chat is open should not change the flag.
371// channel_chat_a373 channel_chat_a
372// .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())374 .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
373// .await375 .await
374// .unwrap();376 .unwrap();
375 377
376// executor.run_until_parked();378 executor.run_until_parked();
377 379
378// let b_has_messages = cx_b.read_with(|cx| {380 let b_has_messages = cx_b.update(|cx| {
379// client_b381 client_b
380// .channel_store()382 .channel_store()
381// .read(cx)383 .read(cx)
382// .has_new_messages(channel_id)384 .has_new_messages(channel_id)
383// .unwrap()385 .unwrap()
384// });386 });
385 387
386// assert!(b_has_messages);388 assert!(b_has_messages);
387 389
388// // Closing the chat should re-enable change tracking390 // Closing the chat should re-enable change tracking
389// cx_b.update(|_| drop(chat_panel_b));391 cx_b.update(|_| drop(chat_panel_b));
390 392
391// channel_chat_a393 channel_chat_a
392// .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())394 .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
393// .await395 .await
394// .unwrap();396 .unwrap();
395 397
396// executor.run_until_parked();398 executor.run_until_parked();
397 399
398// let b_has_messages = cx_b.read_with(|cx| {400 let b_has_messages = cx_b.update(|cx| {
399// client_b401 client_b
400// .channel_store()402 .channel_store()
401// .read(cx)403 .read(cx)
402// .has_new_messages(channel_id)404 .has_new_messages(channel_id)
403// .unwrap()405 .unwrap()
404// });406 });
405 407
406// assert!(b_has_messages);408 assert!(b_has_messages);
407// }409}
crates/collab/src/tests/following_tests.rs+223−6
@@ -1,9 +1,12 @@
1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2use call::ActiveCall;2use call::{ActiveCall, ParticipantLocation};
3use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;3use collab_ui::{
4 channel_view::ChannelView,
5 notifications::project_shared_notification::ProjectSharedNotification,
6};
4use editor::{Editor, ExcerptRange, MultiBuffer};7use editor::{Editor, ExcerptRange, MultiBuffer};
5use gpui::{8use gpui::{
6 point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext,9 point, BackgroundExecutor, Context, Entity, SharedString, TestAppContext, View, VisualContext,
7 VisualTestContext,10 VisualTestContext,
8};11};
9use language::Capability;12use language::Capability;
@@ -1568,6 +1571,59 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut · 1558 unmodified lines
1568 });1571 });
1569}1572}
1570 1573
1574#[gpui::test]
1575async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1576 let (client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1577
1578 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1579 client_a
1580 .host_workspace(&workspace_a, channel_id, cx_a)
1581 .await;
1582 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1583
1584 cx_a.simulate_keystrokes("cmd-p 2 enter");
1585 cx_a.run_until_parked();
1586
1587 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1588 workspace.active_item_as::<Editor>(cx).unwrap()
1589 });
1590 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1591 workspace.active_item_as::<Editor>(cx).unwrap()
1592 });
1593
1594 // b should follow a to position 1
1595 editor_a.update(cx_a, |editor, cx| {
1596 editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
1597 });
1598 cx_a.run_until_parked();
1599 editor_b.update(cx_b, |editor, cx| {
1600 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1601 });
1602
1603 // a unshares the project
1604 cx_a.update(|cx| {
1605 let project = workspace_a.read(cx).project().clone();
1606 ActiveCall::global(cx).update(cx, |call, cx| {
1607 call.unshare_project(project, cx).unwrap();
1608 })
1609 });
1610 cx_a.run_until_parked();
1611
1612 // b should not follow a to position 2
1613 editor_a.update(cx_a, |editor, cx| {
1614 editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
1615 });
1616 cx_a.run_until_parked();
1617 editor_b.update(cx_b, |editor, cx| {
1618 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1619 });
1620 cx_b.update(|cx| {
1621 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1622 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1623 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1624 })
1625}
1626
1571#[gpui::test]1627#[gpui::test]
1572async fn test_following_into_excluded_file(1628async fn test_following_into_excluded_file(
1573 mut cx_a: &mut TestAppContext,1629 mut cx_a: &mut TestAppContext,
@@ -1593,9 +1649,6 @@ async fn test_following_into_excluded_file( · 19 unmodified lines
1593 let active_call_b = cx_b.read(ActiveCall::global);1649 let active_call_b = cx_b.read(ActiveCall::global);
1594 let peer_id_a = client_a.peer_id().unwrap();1650 let peer_id_a = client_a.peer_id().unwrap();
1595 1651
1596 cx_a.update(editor::init);
1597 cx_b.update(editor::init);
1598
1599 client_a1652 client_a
1600 .fs()1653 .fs()
1601 .insert_tree(1654 .insert_tree(
@@ -1772,3 +1825,167 @@ fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Ve · 170 unmodified lines
1772 .collect()1825 .collect()
1773 })1826 })
1774}1827}
1828
1829#[gpui::test(iterations = 10)]
1830async fn test_following_to_channel_notes_without_a_shared_project(
1831 deterministic: BackgroundExecutor,
1832 mut cx_a: &mut TestAppContext,
1833 mut cx_b: &mut TestAppContext,
1834 mut cx_c: &mut TestAppContext,
1835) {
1836 let mut server = TestServer::start(deterministic.clone()).await;
1837 let client_a = server.create_client(cx_a, "user_a").await;
1838 let client_b = server.create_client(cx_b, "user_b").await;
1839 let client_c = server.create_client(cx_c, "user_c").await;
1840
1841 cx_a.update(editor::init);
1842 cx_b.update(editor::init);
1843 cx_c.update(editor::init);
1844 cx_a.update(collab_ui::channel_view::init);
1845 cx_b.update(collab_ui::channel_view::init);
1846 cx_c.update(collab_ui::channel_view::init);
1847
1848 let channel_1_id = server
1849 .make_channel(
1850 "channel-1",
1851 None,
1852 (&client_a, cx_a),
1853 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1854 )
1855 .await;
1856 let channel_2_id = server
1857 .make_channel(
1858 "channel-2",
1859 None,
1860 (&client_a, cx_a),
1861 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1862 )
1863 .await;
1864
1865 // Clients A, B, and C join a channel.
1866 let active_call_a = cx_a.read(ActiveCall::global);
1867 let active_call_b = cx_b.read(ActiveCall::global);
1868 let active_call_c = cx_c.read(ActiveCall::global);
1869 for (call, cx) in [
1870 (&active_call_a, &mut cx_a),
1871 (&active_call_b, &mut cx_b),
1872 (&active_call_c, &mut cx_c),
1873 ] {
1874 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1875 .await
1876 .unwrap();
1877 }
1878 deterministic.run_until_parked();
1879
1880 // Clients A, B, and C all open their own unshared projects.
1881 client_a
1882 .fs()
1883 .insert_tree("/a", json!({ "1.txt": "" }))
1884 .await;
1885 client_b.fs().insert_tree("/b", json!({})).await;
1886 client_c.fs().insert_tree("/c", json!({})).await;
1887 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1888 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1889 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1890 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1891 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1892 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1893
1894 active_call_a
1895 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1896 .await
1897 .unwrap();
1898
1899 // Client A opens the notes for channel 1.
1900 let channel_notes_1_a = cx_a
1901 .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
1902 .await
1903 .unwrap();
1904 channel_notes_1_a.update(cx_a, |notes, cx| {
1905 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1906 notes.editor.update(cx, |editor, cx| {
1907 editor.insert("Hello from A.", cx);
1908 editor.change_selections(None, cx, |selections| {
1909 selections.select_ranges(vec![3..4]);
1910 });
1911 });
1912 });
1913
1914 // Client B follows client A.
1915 workspace_b
1916 .update(cx_b, |workspace, cx| {
1917 workspace
1918 .start_following(client_a.peer_id().unwrap(), cx)
1919 .unwrap()
1920 })
1921 .await
1922 .unwrap();
1923
1924 // Client B is taken to the notes for channel 1, with the same
1925 // text selected as client A.
1926 deterministic.run_until_parked();
1927 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1928 assert_eq!(
1929 workspace.leader_for_pane(workspace.active_pane()),
1930 Some(client_a.peer_id().unwrap())
1931 );
1932 workspace
1933 .active_item(cx)
1934 .expect("no active item")
1935 .downcast::<ChannelView>()
1936 .expect("active item is not a channel view")
1937 });
1938 channel_notes_1_b.update(cx_b, |notes, cx| {
1939 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1940 let editor = notes.editor.read(cx);
1941 assert_eq!(editor.text(cx), "Hello from A.");
1942 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
1943 });
1944
1945 // Client A opens the notes for channel 2.
1946 let channel_notes_2_a = cx_a
1947 .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
1948 .await
1949 .unwrap();
1950 channel_notes_2_a.update(cx_a, |notes, cx| {
1951 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1952 });
1953
1954 // Client B is taken to the notes for channel 2.
1955 deterministic.run_until_parked();
1956 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
1957 assert_eq!(
1958 workspace.leader_for_pane(workspace.active_pane()),
1959 Some(client_a.peer_id().unwrap())
1960 );
1961 workspace
1962 .active_item(cx)
1963 .expect("no active item")
1964 .downcast::<ChannelView>()
1965 .expect("active item is not a channel view")
1966 });
1967 channel_notes_2_b.update(cx_b, |notes, cx| {
1968 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1969 });
1970
1971 // Client A opens a local buffer in their unshared project.
1972 let _unshared_editor_a1 = workspace_a
1973 .update(cx_a, |workspace, cx| {
1974 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1975 })
1976 .await
1977 .unwrap()
1978 .downcast::<Editor>()
1979 .unwrap();
1980
1981 // This does not send any leader update message to client B.
1982 // If it did, an error would occur on client B, since this buffer
1983 // is not shared with them.
1984 deterministic.run_until_parked();
1985 workspace_b.update(cx_b, |workspace, cx| {
1986 assert_eq!(
1987 workspace.active_item(cx).expect("no active item").item_id(),
1988 channel_notes_2_b.entity_id()
1989 );
1990 });
1991}
crates/collab/src/tests/test_server.rs+62−2
@@ -113,6 +113,20 @@ impl TestServer { · 112 unmodified lines
113 }113 }
114 }114 }
115 115
116 pub async fn start2(
117 cx_a: &mut TestAppContext,
118 cx_b: &mut TestAppContext,
119 ) -> (TestClient, TestClient, u64) {
120 let mut server = Self::start(cx_a.executor()).await;
121 let client_a = server.create_client(cx_a, "user_a").await;
122 let client_b = server.create_client(cx_b, "user_b").await;
123 let channel_id = server
124 .make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
125 .await;
126
127 (client_a, client_b, channel_id)
128 }
129
116 pub async fn reset(&self) {130 pub async fn reset(&self) {
117 self.app_state.db.reset();131 self.app_state.db.reset();
118 let epoch = self132 let epoch = self
@@ -619,14 +633,49 @@ impl TestClient { · 500 unmodified lines
619 "/a",633 "/a",
620 json!({634 json!({
621 "1.txt": "one\none\none",635 "1.txt": "one\none\none",
622 "2.txt": "two\ntwo\ntwo",636 "2.js": "function two() { return 2; }",
623 "3.txt": "three\nthree\nthree",637 "3.rs": "mod test",
624 }),638 }),
625 )639 )
626 .await;640 .await;
627 self.build_local_project("/a", cx).await.0641 self.build_local_project("/a", cx).await.0
628 }642 }
629 643
644 pub async fn host_workspace(
645 &self,
646 workspace: &View<Workspace>,
647 channel_id: u64,
648 cx: &mut VisualTestContext,
649 ) {
650 cx.update(|cx| {
651 let active_call = ActiveCall::global(cx);
652 active_call.update(cx, |call, cx| call.join_channel(channel_id, cx))
653 })
654 .await
655 .unwrap();
656 cx.update(|cx| {
657 let active_call = ActiveCall::global(cx);
658 let project = workspace.read(cx).project().clone();
659 active_call.update(cx, |call, cx| call.share_project(project, cx))
660 })
661 .await
662 .unwrap();
663 cx.executor().run_until_parked();
664 }
665
666 pub async fn join_workspace<'a>(
667 &'a self,
668 channel_id: u64,
669 cx: &'a mut TestAppContext,
670 ) -> (View<Workspace>, &'a mut VisualTestContext) {
671 cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
672 .await
673 .unwrap();
674 cx.run_until_parked();
675
676 self.active_workspace(cx)
677 }
678
630 pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {679 pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
631 cx.update(|cx| {680 cx.update(|cx| {
632 Project::local(681 Project::local(
@@ -670,6 +719,17 @@ impl TestClient { · 37 unmodified lines
670 })719 })
671 }720 }
672 721
722 pub async fn build_test_workspace<'a>(
723 &'a self,
724 cx: &'a mut TestAppContext,
725 ) -> (View<Workspace>, &'a mut VisualTestContext) {
726 let project = self.build_test_project(cx).await;
727 cx.add_window_view(|cx| {
728 cx.activate_window();
729 Workspace::new(0, project.clone(), self.app_state.clone(), cx)
730 })
731 }
732
673 pub fn active_workspace<'a>(733 pub fn active_workspace<'a>(
674 &'a self,734 &'a self,
675 cx: &'a mut TestAppContext,735 cx: &'a mut TestAppContext,
crates/collab_ui/src/collab_ui.rs+0−1
@@ -111,7 +111,6 @@ fn notification_window_options( · 110 unmodified lines
111 let screen_bounds = screen.bounds();111 let screen_bounds = screen.bounds();
112 let size: Size<GlobalPixels> = window_size.into();112 let size: Size<GlobalPixels> = window_size.into();
113 113
114 // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
115 let bounds = gpui::Bounds::<GlobalPixels> {114 let bounds = gpui::Bounds::<GlobalPixels> {
116 origin: screen_bounds.upper_right()115 origin: screen_bounds.upper_right()
117 - point(116 - point(
crates/editor/src/display_map/wrap_map.rs+24−30
@@ -11,7 +11,6 @@ use smol::future::yield_now; · 10 unmodified lines
11use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};11use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
12use sum_tree::{Bias, Cursor, SumTree};12use sum_tree::{Bias, Cursor, SumTree};
13use text::Patch;13use text::Patch;
14use util::ResultExt;
15 14
16pub use super::tab_map::TextSummary;15pub use super::tab_map::TextSummary;
17pub type WrapEdit = text::Edit<u32>;16pub type WrapEdit = text::Edit<u32>;
@@ -154,26 +153,24 @@ impl WrapMap { · 136 unmodified lines
154 153
155 if let Some(wrap_width) = self.wrap_width {154 if let Some(wrap_width) = self.wrap_width {
156 let mut new_snapshot = self.snapshot.clone();155 let mut new_snapshot = self.snapshot.clone();
157 let mut edits = Patch::default();156
158 let text_system = cx.text_system().clone();157 let text_system = cx.text_system().clone();
159 let (font, font_size) = self.font_with_size.clone();158 let (font, font_size) = self.font_with_size.clone();
160 let task = cx.background_executor().spawn(async move {159 let task = cx.background_executor().spawn(async move {
161 if let Some(mut line_wrapper) = text_system.line_wrapper(font, font_size).log_err()160 let mut line_wrapper = text_system.line_wrapper(font, font_size);
162 {161 let tab_snapshot = new_snapshot.tab_snapshot.clone();
163 let tab_snapshot = new_snapshot.tab_snapshot.clone();162 let range = TabPoint::zero()..tab_snapshot.max_point();
164 let range = TabPoint::zero()..tab_snapshot.max_point();163 let edits = new_snapshot
165 edits = new_snapshot164 .update(
166 .update(165 tab_snapshot,
167 tab_snapshot,166 &[TabEdit {
168 &[TabEdit {167 old: range.clone(),
169 old: range.clone(),168 new: range.clone(),
170 new: range.clone(),169 }],
171 }],170 wrap_width,
172 wrap_width,171 &mut line_wrapper,
173 &mut line_wrapper,172 )
174 )173 .await;
175 .await;
176 }
177 (new_snapshot, edits)174 (new_snapshot, edits)
178 });175 });
179 176
@@ -245,15 +242,12 @@ impl WrapMap { · 65 unmodified lines
245 let (font, font_size) = self.font_with_size.clone();242 let (font, font_size) = self.font_with_size.clone();
246 let update_task = cx.background_executor().spawn(async move {243 let update_task = cx.background_executor().spawn(async move {
247 let mut edits = Patch::default();244 let mut edits = Patch::default();
248 if let Some(mut line_wrapper) =245 let mut line_wrapper = text_system.line_wrapper(font, font_size);
249 text_system.line_wrapper(font, font_size).log_err()246 for (tab_snapshot, tab_edits) in pending_edits {
250 {247 let wrap_edits = snapshot
251 for (tab_snapshot, tab_edits) in pending_edits {248 .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
252 let wrap_edits = snapshot249 .await;
253 .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)250 edits = edits.compose(&wrap_edits);
254 .await;
255 edits = edits.compose(&wrap_edits);
256 }
257 }251 }
258 (snapshot, edits)252 (snapshot, edits)
259 });253 });
@@ -1043,7 +1037,7 @@ mod tests { · 783 unmodified lines
1043 1037
1044 #[gpui::test(iterations = 100)]1038 #[gpui::test(iterations = 100)]
1045 async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {1039 async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
1046 // todo!() this test is flaky1040 // todo this test is flaky
1047 init_test(cx);1041 init_test(cx);
1048 1042
1049 cx.background_executor.set_block_on_ticks(0..=50);1043 cx.background_executor.set_block_on_ticks(0..=50);
@@ -1059,7 +1053,7 @@ mod tests { · 9 unmodified lines
1059 };1053 };
1060 let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();1054 let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
1061 let font = font("Helvetica");1055 let font = font("Helvetica");
1062 let _font_id = text_system.font_id(&font).unwrap();1056 let _font_id = text_system.font_id(&font);
1063 let font_size = px(14.0);1057 let font_size = px(14.0);
1064 1058
1065 log::info!("Tab size: {}", tab_size);1059 log::info!("Tab size: {}", tab_size);
@@ -1086,7 +1080,7 @@ mod tests { · 20 unmodified lines
1086 let tabs_snapshot = tab_map.set_max_expansion_column(32);1080 let tabs_snapshot = tab_map.set_max_expansion_column(32);
1087 log::info!("TabMap text: {:?}", tabs_snapshot.text());1081 log::info!("TabMap text: {:?}", tabs_snapshot.text());
1088 1082
1089 let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap();1083 let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
1090 let unwrapped_text = tabs_snapshot.text();1084 let unwrapped_text = tabs_snapshot.text();
1091 let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);1085 let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
1092 1086
crates/editor/src/editor.rs+4−2
@@ -7677,7 +7677,6 @@ impl Editor { · 7676 unmodified lines
7677 scrollbar_width: cx.editor_style.scrollbar_width,7677 scrollbar_width: cx.editor_style.scrollbar_width,
7678 syntax: cx.editor_style.syntax.clone(),7678 syntax: cx.editor_style.syntax.clone(),
7679 status: cx.editor_style.status.clone(),7679 status: cx.editor_style.status.clone(),
7680 // todo!("what about the rest of the highlight style parts for inlays and suggestions?")
7681 inlays_style: HighlightStyle {7680 inlays_style: HighlightStyle {
7682 color: Some(cx.theme().status().hint),7681 color: Some(cx.theme().status().hint),
7683 font_weight: Some(FontWeight::BOLD),7682 font_weight: Some(FontWeight::BOLD),
@@ -8678,6 +8677,10 @@ impl Editor { · 994 unmodified lines
8678 }8677 }
8679 }8678 }
8680 }8679 }
8680
8681 let Some(project) = &self.project else { return };
8682 let telemetry = project.read(cx).client().telemetry().clone();
8683 telemetry.log_edit_event("editor");
8681 }8684 }
8682 multi_buffer::Event::ExcerptsAdded {8685 multi_buffer::Event::ExcerptsAdded {
8683 buffer,8686 buffer,
@@ -9346,7 +9349,6 @@ impl Render for Editor { · 662 unmodified lines
9346 scrollbar_width: px(12.),9349 scrollbar_width: px(12.),
9347 syntax: cx.theme().syntax().clone(),9350 syntax: cx.theme().syntax().clone(),
9348 status: cx.theme().status().clone(),9351 status: cx.theme().status().clone(),
9349 // todo!("what about the rest of the highlight style parts?")
9350 inlays_style: HighlightStyle {9352 inlays_style: HighlightStyle {
9351 color: Some(cx.theme().status().hint),9353 color: Some(cx.theme().status().hint),
9352 font_weight: Some(FontWeight::BOLD),9354 font_weight: Some(FontWeight::BOLD),
crates/editor/src/element.rs+38−38
@@ -809,13 +809,18 @@ impl EditorElement { · 808 unmodified lines
809 // the hunk might include the rows of that header.809 // the hunk might include the rows of that header.
810 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.810 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
811 // Instead, we simply check whether the range we're dealing with includes811 // Instead, we simply check whether the range we're dealing with includes
812 // any custom elements and if so, we stop painting the diff hunk on the first row of that custom element.812 // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
813 let end_row_in_current_excerpt = layout813 let end_row_in_current_excerpt = layout
814 .position_map814 .position_map
815 .snapshot815 .snapshot
816 .blocks_in_range(start_row..end_row)816 .blocks_in_range(start_row..end_row)
817 .next()817 .find_map(|(start_row, block)| {
818 .map(|(start_row, _)| start_row)818 if matches!(block, TransformBlock::ExcerptHeader { .. }) {
819 Some(start_row)
820 } else {
821 None
822 }
823 })
819 .unwrap_or(end_row);824 .unwrap_or(end_row);
820 825
821 let start_y = start_row as f32 * line_height - scroll_top;826 let start_y = start_row as f32 * line_height - scroll_top;
@@ -878,16 +883,23 @@ impl EditorElement { · 56 unmodified lines
878 let fold_corner_radius = 0.15 * layout.position_map.line_height;883 let fold_corner_radius = 0.15 * layout.position_map.line_height;
879 cx.with_element_id(Some("folds"), |cx| {884 cx.with_element_id(Some("folds"), |cx| {
880 let snapshot = &layout.position_map.snapshot;885 let snapshot = &layout.position_map.snapshot;
886
881 for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) {887 for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) {
882 let fold_range = fold.range.clone();888 let fold_range = fold.range.clone();
883 let display_range = fold.range.start.to_display_point(&snapshot)889 let display_range = fold.range.start.to_display_point(&snapshot)
884 ..fold.range.end.to_display_point(&snapshot);890 ..fold.range.end.to_display_point(&snapshot);
885 debug_assert_eq!(display_range.start.row(), display_range.end.row());891 debug_assert_eq!(display_range.start.row(), display_range.end.row());
886 let row = display_range.start.row();892 let row = display_range.start.row();
893 debug_assert!(row < layout.visible_display_row_range.end);
894 let Some(line_layout) = &layout
895 .position_map
896 .line_layouts
897 .get((row - layout.visible_display_row_range.start) as usize)
898 .map(|l| &l.line)
899 else {
900 continue;
901 };
887 902
888 let line_layout = &layout.position_map.line_layouts
889 [(row - layout.visible_display_row_range.start) as usize]
890 .line;
891 let start_x = content_origin.x903 let start_x = content_origin.x
892 + line_layout.x_for_index(display_range.start.column() as usize)904 + line_layout.x_for_index(display_range.start.column() as usize)
893 - layout.position_map.scroll_position.x;905 - layout.position_map.scroll_position.x;
@@ -1010,7 +1022,6 @@ impl EditorElement { · 116 unmodified lines
1010 .chars_at(cursor_position)1022 .chars_at(cursor_position)
1011 .next()1023 .next()
1012 .and_then(|(character, _)| {1024 .and_then(|(character, _)| {
1013 // todo!() currently shape_line panics if text conatins newlines
1014 let text = if character == '\n' {1025 let text = if character == '\n' {
1015 SharedString::from(" ")1026 SharedString::from(" ")
1016 } else {1027 } else {
@@ -2258,11 +2269,9 @@ impl EditorElement { · 1241 unmodified lines
2258 .map_or(range.context.start, |primary| primary.start);2269 .map_or(range.context.start, |primary| primary.start);
2259 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);2270 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
2260 2271
2261 let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| {2272 cx.listener_for(&self.editor, move |editor, _, cx| {
2262 editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);2273 editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
2263 });2274 })
2264
2265 jump_handler
2266 });2275 });
2267 2276
2268 let element = if *starts_new_buffer {2277 let element = if *starts_new_buffer {
@@ -2342,34 +2351,25 @@ impl EditorElement { · 73 unmodified lines
2342 .text_color(cx.theme().colors().editor_line_number)2351 .text_color(cx.theme().colors().editor_line_number)
2343 .child("..."),2352 .child("..."),
2344 )2353 )
2345 .map(|this| {2354 .child(
2346 if let Some(jump_handler) = jump_handler {2355 ButtonLike::new("jump to collapsed context")
2347 this.child(2356 .style(ButtonStyle::Transparent)
2348 ButtonLike::new("jump to collapsed context")2357 .full_width()
2349 .style(ButtonStyle::Transparent)2358 .child(
2350 .full_width()2359 div()
2351 .on_click(jump_handler)2360 .h_px()
2352 .tooltip(|cx| {2361 .w_full()
2353 Tooltip::for_action(2362 .bg(cx.theme().colors().border_variant)
2354 "Jump to Buffer",2363 .group_hover("", |style| {
2355 &OpenExcerpts,2364 style.bg(cx.theme().colors().border)
2356 cx,2365 }),
2357 )
2358 })
2359 .child(
2360 div()
2361 .h_px()
2362 .w_full()
2363 .bg(cx.theme().colors().border_variant)
2364 .group_hover("", |style| {
2365 style.bg(cx.theme().colors().border)
2366 }),
2367 ),
2368 )2366 )
2369 } else {2367 .when_some(jump_handler, |this, jump_handler| {
2370 this.child(div().size_full().bg(gpui::green()))2368 this.on_click(jump_handler).tooltip(|cx| {
2371 }2369 Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
2372 })2370 })
2371 }),
2372 )
2373 };2373 };
2374 element.into_any()2374 element.into_any()
2375 }2375 }
crates/editor/src/hover_popover.rs+4−6
@@ -2,13 +2,13 @@ use crate::{ · 1 unmodified line
2 display_map::{InlayOffset, ToDisplayPoint},2 display_map::{InlayOffset, ToDisplayPoint},
3 link_go_to_definition::{InlayHighlight, RangeInEditor},3 link_go_to_definition::{InlayHighlight, RangeInEditor},
4 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,4 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
5 ExcerptId, RangeToAnchorExt,5 ExcerptId, Hover, RangeToAnchorExt,
6};6};
7use futures::FutureExt;7use futures::FutureExt;
8use gpui::{8use gpui::{
9 actions, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model,9 div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, MouseButton,
10 MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,10 ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
11 Task, ViewContext, WeakView,11 ViewContext, WeakView,
12};12};
13use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};13use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
14 14
@@ -27,8 +27,6 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; · 12 unmodified lines
27pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);27pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
28pub const HOVER_POPOVER_GAP: Pixels = px(10.);28pub const HOVER_POPOVER_GAP: Pixels = px(10.);
29 29
30actions!(editor, [Hover]);
31
32/// Bindable action which uses the most recent selection head to trigger a hover30/// Bindable action which uses the most recent selection head to trigger a hover
33pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {31pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
34 let head = editor.selections.newest_display(cx).head();32 let head = editor.selections.newest_display(cx).head();
crates/editor/src/items.rs+3−1
@@ -82,7 +82,9 @@ impl FollowableItem for Editor { · 81 unmodified lines
82 82
83 let pane = pane.downgrade();83 let pane = pane.downgrade();
84 Some(cx.spawn(|mut cx| async move {84 Some(cx.spawn(|mut cx| async move {
85 let mut buffers = futures::future::try_join_all(buffers).await?;85 let mut buffers = futures::future::try_join_all(buffers)
86 .await
87 .debug_assert_ok("leaders don't share views for unshared buffers")?;
86 let editor = pane.update(&mut cx, |pane, cx| {88 let editor = pane.update(&mut cx, |pane, cx| {
87 let mut editors = pane.items_of_type::<Self>();89 let mut editors = pane.items_of_type::<Self>();
88 editors.find(|editor| {90 editors.find(|editor| {
crates/feedback/src/feedback_modal.rs+1−40
@@ -525,43 +525,4 @@ impl Render for FeedbackModal { · 524 unmodified lines
525 }525 }
526}526}
527 527
528// TODO: Testing of various button states, dismissal prompts, etc.528// TODO: Testing of various button states, dismissal prompts, etc. :)
529
530// #[cfg(test)]
531// mod test {
532// use super::*;
533
534// #[test]
535// fn test_invalid_email_addresses() {
536// let markdown = markdown.await.log_err();
537// let buffer = project.update(&mut cx, |project, cx| {
538// project.create_buffer("", markdown, cx)
539// })??;
540
541// workspace.update(&mut cx, |workspace, cx| {
542// let system_specs = SystemSpecs::new(cx);
543
544// workspace.toggle_modal(cx, move |cx| {
545// let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx);
546
547// assert!(!feedback_modal.can_submit());
548// assert!(!feedback_modal.valid_email_address(cx));
549// assert!(!feedback_modal.valid_character_count());
550
551// feedback_modal
552// .email_address_editor
553// .update(cx, |this, cx| this.set_text("a", cx));
554// feedback_modal.set_submission_state(cx);
555
556// assert!(!feedback_modal.valid_email_address(cx));
557
558// feedback_modal
559// .email_address_editor
560// .update(cx, |this, cx| this.set_text("a&b.com", cx));
561// feedback_modal.set_submission_state(cx);
562
563// assert!(feedback_modal.valid_email_address(cx));
564// });
565// })?;
566// }
567// }
crates/file_finder/Cargo.toml+1−0
@@ -23,6 +23,7 @@ theme = { path = "../theme" } · 22 unmodified lines
23ui = { path = "../ui" }23ui = { path = "../ui" }
24workspace = { path = "../workspace" }24workspace = { path = "../workspace" }
25postage.workspace = true25postage.workspace = true
26anyhow.workspace = true
26serde.workspace = true27serde.workspace = true
27 28
28[dev-dependencies]29[dev-dependencies]
crates/file_finder/src/file_finder.rs+125−2
@@ -312,7 +312,7 @@ impl FileFinderDelegate { · 311 unmodified lines
312 cx: &mut ViewContext<FileFinder>,312 cx: &mut ViewContext<FileFinder>,
313 ) -> Self {313 ) -> Self {
314 cx.observe(&project, |file_finder, _, cx| {314 cx.observe(&project, |file_finder, _, cx| {
315 //todo!() We should probably not re-render on every project anything315 //todo We should probably not re-render on every project anything
316 file_finder316 file_finder
317 .picker317 .picker
318 .update(cx, |picker, cx| picker.refresh(cx))318 .update(cx, |picker, cx| picker.refresh(cx))
@@ -519,6 +519,62 @@ impl FileFinderDelegate { · 200 unmodified lines
519 519
520 (file_name, file_name_positions, full_path, path_positions)520 (file_name, file_name_positions, full_path, path_positions)
521 }521 }
522
523 fn lookup_absolute_path(
524 &self,
525 query: PathLikeWithPosition<FileSearchQuery>,
526 cx: &mut ViewContext<'_, Picker<Self>>,
527 ) -> Task<()> {
528 cx.spawn(|picker, mut cx| async move {
529 let Some((project, fs)) = picker
530 .update(&mut cx, |picker, cx| {
531 let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
532 (picker.delegate.project.clone(), fs)
533 })
534 .log_err()
535 else {
536 return;
537 };
538
539 let query_path = Path::new(query.path_like.path_query());
540 let mut path_matches = Vec::new();
541 match fs.metadata(query_path).await.log_err() {
542 Some(Some(_metadata)) => {
543 let update_result = project
544 .update(&mut cx, |project, cx| {
545 if let Some((worktree, relative_path)) =
546 project.find_local_worktree(query_path, cx)
547 {
548 path_matches.push(PathMatch {
549 score: 0.0,
550 positions: Vec::new(),
551 worktree_id: worktree.read(cx).id().to_usize(),
552 path: Arc::from(relative_path),
553 path_prefix: "".into(),
554 distance_to_relative_ancestor: usize::MAX,
555 });
556 }
557 })
558 .log_err();
559 if update_result.is_none() {
560 return;
561 }
562 }
563 Some(None) => {}
564 None => return,
565 }
566
567 picker
568 .update(&mut cx, |picker, cx| {
569 let picker_delegate = &mut picker.delegate;
570 let search_id = util::post_inc(&mut picker_delegate.search_count);
571 picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
572
573 anyhow::Ok(())
574 })
575 .log_err();
576 })
577 }
522}578}
523 579
524impl PickerDelegate for FileFinderDelegate {580impl PickerDelegate for FileFinderDelegate {
@@ -588,7 +644,12 @@ impl PickerDelegate for FileFinderDelegate { · 63 unmodified lines
588 })644 })
589 })645 })
590 .expect("infallible");646 .expect("infallible");
591 self.spawn_search(query, cx)647
648 if Path::new(query.path_like.path_query()).is_absolute() {
649 self.lookup_absolute_path(query, cx)
650 } else {
651 self.spawn_search(query, cx)
652 }
592 }653 }
593 }654 }
594 655
@@ -818,6 +879,68 @@ mod tests { · 223 unmodified lines
818 }879 }
819 }880 }
820 881
882 #[gpui::test]
883 async fn test_absolute_paths(cx: &mut TestAppContext) {
884 let app_state = init_test(cx);
885 app_state
886 .fs
887 .as_fake()
888 .insert_tree(
889 "/root",
890 json!({
891 "a": {
892 "file1.txt": "",
893 "b": {
894 "file2.txt": "",
895 },
896 }
897 }),
898 )
899 .await;
900
901 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
902
903 let (picker, workspace, cx) = build_find_picker(project, cx);
904
905 let matching_abs_path = "/root/a/b/file2.txt";
906 picker
907 .update(cx, |picker, cx| {
908 picker
909 .delegate
910 .update_matches(matching_abs_path.to_string(), cx)
911 })
912 .await;
913 picker.update(cx, |picker, _| {
914 assert_eq!(
915 collect_search_results(picker),
916 vec![PathBuf::from("a/b/file2.txt")],
917 "Matching abs path should be the only match"
918 )
919 });
920 cx.dispatch_action(SelectNext);
921 cx.dispatch_action(Confirm);
922 cx.read(|cx| {
923 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
924 assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
925 });
926
927 let mismatching_abs_path = "/root/a/b/file1.txt";
928 picker
929 .update(cx, |picker, cx| {
930 picker
931 .delegate
932 .update_matches(mismatching_abs_path.to_string(), cx)
933 })
934 .await;
935 picker.update(cx, |picker, _| {
936 assert_eq!(
937 collect_search_results(picker),
938 Vec::<PathBuf>::new(),
939 "Mismatching abs path should produce no matches"
940 )
941 });
942 }
943
821 #[gpui::test]944 #[gpui::test]
822 async fn test_complex_path(cx: &mut TestAppContext) {945 async fn test_complex_path(cx: &mut TestAppContext) {
823 let app_state = init_test(cx);946 let app_state = init_test(cx);
crates/gpui/src/action.rs+1−1
@@ -170,7 +170,7 @@ impl ActionRegistry { · 169 unmodified lines
170macro_rules! actions {170macro_rules! actions {
171 ($namespace:path, [ $($name:ident),* $(,)? ]) => {171 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
172 $(172 $(
173 #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)]173 #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
174 #[serde(crate = "gpui::private::serde")]174 #[serde(crate = "gpui::private::serde")]
175 pub struct $name;175 pub struct $name;
176 176
crates/gpui/src/keymap/matcher.rs+375−370
@@ -28,11 +28,11 @@ impl KeystrokeMatcher { · 27 unmodified lines
28 28
29 /// Pushes a keystroke onto the matcher.29 /// Pushes a keystroke onto the matcher.
30 /// The result of the new keystroke is returned:30 /// The result of the new keystroke is returned:
31 /// KeyMatch::None =>31 /// - KeyMatch::None =>
32 /// No match is valid for this key given any pending keystrokes.32 /// No match is valid for this key given any pending keystrokes.
33 /// KeyMatch::Pending =>33 /// - KeyMatch::Pending =>
34 /// There exist bindings which are still waiting for more keys.34 /// There exist bindings which are still waiting for more keys.
35 /// KeyMatch::Complete(matches) =>35 /// - KeyMatch::Complete(matches) =>
36 /// One or more bindings have received the necessary key presses.36 /// One or more bindings have received the necessary key presses.
37 /// Bindings added later will take precedence over earlier bindings.37 /// Bindings added later will take precedence over earlier bindings.
38 pub fn match_keystroke(38 pub fn match_keystroke(
@@ -77,12 +77,10 @@ impl KeystrokeMatcher { · 38 unmodified lines
77 77
78 if let Some(pending_key) = pending_key {78 if let Some(pending_key) = pending_key {
79 self.pending_keystrokes.push(pending_key);79 self.pending_keystrokes.push(pending_key);
80 }
81
82 if self.pending_keystrokes.is_empty() {
83 KeyMatch::None
84 } else {
85 KeyMatch::Pending80 KeyMatch::Pending
81 } else {
82 self.pending_keystrokes.clear();
83 KeyMatch::None
86 }84 }
87 }85 }
88}86}
@@ -98,367 +96,374 @@ impl KeyMatch { · 9 unmodified lines
98 pub fn is_some(&self) -> bool {96 pub fn is_some(&self) -> bool {
99 matches!(self, KeyMatch::Some(_))97 matches!(self, KeyMatch::Some(_))
100 }98 }
99
100 pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
101 match self {
102 KeyMatch::Some(matches) => Some(matches),
103 _ => None,
104 }
105 }
106}
107
108impl PartialEq for KeyMatch {
109 fn eq(&self, other: &Self) -> bool {
110 match (self, other) {
111 (KeyMatch::None, KeyMatch::None) => true,
112 (KeyMatch::Pending, KeyMatch::Pending) => true,
113 (KeyMatch::Some(a), KeyMatch::Some(b)) => {
114 if a.len() != b.len() {
115 return false;
116 }
117
118 for (a, b) in a.iter().zip(b.iter()) {
119 if !a.partial_eq(b.as_ref()) {
120 return false;
121 }
122 }
123
124 true
125 }
126 _ => false,
127 }
128 }
101}129}
102 130
103// #[cfg(test)]131#[cfg(test)]
104// mod tests {132mod tests {
105// use anyhow::Result;133
106// use serde::Deserialize;134 use serde_derive::Deserialize;
107 135
108// use crate::{actions, impl_actions, keymap_matcher::ActionContext};136 use super::*;
109 137 use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
110// use super::*;138 use crate::{actions, KeyBinding};
111 139
112// #[test]140 #[test]
113// fn test_keymap_and_view_ordering() -> Result<()> {141 fn test_keymap_and_view_ordering() {
114// actions!(test, [EditorAction, ProjectPanelAction]);142 actions!(test, [EditorAction, ProjectPanelAction]);
115 143
116// let mut editor = ActionContext::default();144 let mut editor = KeyContext::default();
117// editor.add_identifier("Editor");145 editor.add("Editor");
118 146
119// let mut project_panel = ActionContext::default();147 let mut project_panel = KeyContext::default();
120// project_panel.add_identifier("ProjectPanel");148 project_panel.add("ProjectPanel");
121 149
122// // Editor 'deeper' in than project panel150 // Editor 'deeper' in than project panel
123// let dispatch_path = vec![(2, editor), (1, project_panel)];151 let dispatch_path = vec![project_panel, editor];
124 152
125// // But editor actions 'higher' up in keymap153 // But editor actions 'higher' up in keymap
126// let keymap = Keymap::new(vec![154 let keymap = Keymap::new(vec![
127// Binding::new("left", EditorAction, Some("Editor")),155 KeyBinding::new("left", EditorAction, Some("Editor")),
128// Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),156 KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
129// ]);157 ]);
130 158
131// let mut matcher = KeymapMatcher::new(keymap);159 let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
132 160
133// assert_eq!(161 let matches = matcher
134// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),162 .match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
135// KeyMatch::Matches(vec![163 .matches()
136// (2, Box::new(EditorAction)),164 .unwrap();
137// (1, Box::new(ProjectPanelAction)),165
138// ]),166 assert!(matches[0].partial_eq(&EditorAction));
139// );167 assert!(matches.get(1).is_none());
140 168 }
141// Ok(())169
142// }170 #[test]
143 171 fn test_multi_keystroke_match() {
144// #[test]172 actions!(test, [B, AB, C, D, DA, E, EF]);
145// fn test_push_keystroke() -> Result<()> {173
146// actions!(test, [B, AB, C, D, DA, E, EF]);174 let mut context1 = KeyContext::default();
147 175 context1.add("1");
148// let mut context1 = ActionContext::default();176
149// context1.add_identifier("1");177 let mut context2 = KeyContext::default();
150 178 context2.add("2");
151// let mut context2 = ActionContext::default();179
152// context2.add_identifier("2");180 let dispatch_path = vec![context2, context1];
153 181
154// let dispatch_path = vec![(2, context2), (1, context1)];182 let keymap = Keymap::new(vec![
155 183 KeyBinding::new("a b", AB, Some("1")),
156// let keymap = Keymap::new(vec![184 KeyBinding::new("b", B, Some("2")),
157// Binding::new("a b", AB, Some("1")),185 KeyBinding::new("c", C, Some("2")),
158// Binding::new("b", B, Some("2")),186 KeyBinding::new("d", D, Some("1")),
159// Binding::new("c", C, Some("2")),187 KeyBinding::new("d", D, Some("2")),
160// Binding::new("d", D, Some("1")),188 KeyBinding::new("d a", DA, Some("2")),
161// Binding::new("d", D, Some("2")),189 ]);
162// Binding::new("d a", DA, Some("2")),190
163// ]);191 let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
164 192
165// let mut matcher = KeymapMatcher::new(keymap);193 // Binding with pending prefix always takes precedence
166 194 assert_eq!(
167// // Binding with pending prefix always takes precedence195 matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
168// assert_eq!(196 KeyMatch::Pending,
169// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),197 );
170// KeyMatch::Pending,198 // B alone doesn't match because a was pending, so AB is returned instead
171// );199 assert_eq!(
172// // B alone doesn't match because a was pending, so AB is returned instead200 matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path),
173// assert_eq!(201 KeyMatch::Some(vec![Box::new(AB)]),
174// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),202 );
175// KeyMatch::Matches(vec![(1, Box::new(AB))]),203 assert!(!matcher.has_pending_keystrokes());
176// );204
177// assert!(!matcher.has_pending_keystrokes());205 // Without an a prefix, B is dispatched like expected
178 206 assert_eq!(
179// // Without an a prefix, B is dispatched like expected207 matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]),
180// assert_eq!(208 KeyMatch::Some(vec![Box::new(B)]),
181// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),209 );
182// KeyMatch::Matches(vec![(2, Box::new(B))]),210 assert!(!matcher.has_pending_keystrokes());
183// );211
184// assert!(!matcher.has_pending_keystrokes());212 eprintln!("PROBLEM AREA");
185 213 // If a is prefixed, C will not be dispatched because there
186// // If a is prefixed, C will not be dispatched because there214 // was a pending binding for it
187// // was a pending binding for it215 assert_eq!(
188// assert_eq!(216 matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
189// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),217 KeyMatch::Pending,
190// KeyMatch::Pending,218 );
191// );219 assert_eq!(
192// assert_eq!(220 matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path),
193// matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),221 KeyMatch::None,
194// KeyMatch::None,222 );
195// );223 assert!(!matcher.has_pending_keystrokes());
196// assert!(!matcher.has_pending_keystrokes());224
197 225 // If a single keystroke matches multiple bindings in the tree
198// // If a single keystroke matches multiple bindings in the tree226 // only one of them is returned.
199// // all of them are returned so that we can fallback if the action227 assert_eq!(
200// // handler decides to propagate the action228 matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path),
201// assert_eq!(229 KeyMatch::Some(vec![Box::new(D)]),
202// matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),230 );
203// KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),231 }
204// );232
205 233 #[test]
206// // If none of the d action handlers consume the binding, a pending234 fn test_keystroke_parsing() {
207// // binding may then be used235 assert_eq!(
208// assert_eq!(236 Keystroke::parse("ctrl-p").unwrap(),
209// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),237 Keystroke {
210// KeyMatch::Matches(vec![(2, Box::new(DA))]),238 key: "p".into(),
211// );239 modifiers: Modifiers {
212// assert!(!matcher.has_pending_keystrokes());240 control: true,
213 241 alt: false,
214// Ok(())242 shift: false,
215// }243 command: false,
216 244 function: false,
217// #[test]245 },
218// fn test_keystroke_parsing() -> Result<()> {246 ime_key: None,
219// assert_eq!(247 }
220// Keystroke::parse("ctrl-p")?,248 );
221// Keystroke {249
222// key: "p".into(),250 assert_eq!(
223// ctrl: true,251 Keystroke::parse("alt-shift-down").unwrap(),
224// alt: false,252 Keystroke {
225// shift: false,253 key: "down".into(),
226// cmd: false,254 modifiers: Modifiers {
227// function: false,255 control: false,
228// ime_key: None,256 alt: true,
229// }257 shift: true,
230// );258 command: false,
231 259 function: false,
232// assert_eq!(260 },
233// Keystroke::parse("alt-shift-down")?,261 ime_key: None,
234// Keystroke {262 }
235// key: "down".into(),263 );
236// ctrl: false,264
237// alt: true,265 assert_eq!(
238// shift: true,266 Keystroke::parse("shift-cmd--").unwrap(),
239// cmd: false,267 Keystroke {
240// function: false,268 key: "-".into(),
241// ime_key: None,269 modifiers: Modifiers {
242// }270 control: false,
243// );271 alt: false,
244 272 shift: true,
245// assert_eq!(273 command: true,
246// Keystroke::parse("shift-cmd--")?,274 function: false,
247// Keystroke {275 },
248// key: "-".into(),276 ime_key: None,
249// ctrl: false,277 }
250// alt: false,278 );
251// shift: true,279 }
252// cmd: true,280
253// function: false,281 #[test]
254// ime_key: None,282 fn test_context_predicate_parsing() {
255// }283 use KeyBindingContextPredicate::*;
256// );284
257 285 assert_eq!(
258// Ok(())286 KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
259// }287 And(
260 288 Box::new(Identifier("a".into())),
261// #[test]289 Box::new(Or(
262// fn test_context_predicate_parsing() -> Result<()> {290 Box::new(Equal("b".into(), "c".into())),
263// use KeymapContextPredicate::*;291 Box::new(NotEqual("d".into(), "e".into())),
264 292 ))
265// assert_eq!(293 )
266// KeymapContextPredicate::parse("a && (b == c || d != e)")?,294 );
267// And(295
268// Box::new(Identifier("a".into())),296 assert_eq!(
269// Box::new(Or(297 KeyBindingContextPredicate::parse("!a").unwrap(),
270// Box::new(Equal("b".into(), "c".into())),298 Not(Box::new(Identifier("a".into())),)
271// Box::new(NotEqual("d".into(), "e".into())),299 );
272// ))300 }
273// )301
274// );302 #[test]
275 303 fn test_context_predicate_eval() {
276// assert_eq!(304 let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
277// KeymapContextPredicate::parse("!a")?,305
278// Not(Box::new(Identifier("a".into())),)306 let mut context = KeyContext::default();
279// );307 context.add("a");
280 308 assert!(!predicate.eval(&[context]));
281// Ok(())309
282// }310 let mut context = KeyContext::default();
283 311 context.add("a");
284// #[test]312 context.add("b");
285// fn test_context_predicate_eval() {313 assert!(predicate.eval(&[context]));
286// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();314
287 315 let mut context = KeyContext::default();
288// let mut context = ActionContext::default();316 context.add("a");
289// context.add_identifier("a");317 context.set("c", "x");
290// assert!(!predicate.eval(&[context]));318 assert!(!predicate.eval(&[context]));
291 319
292// let mut context = ActionContext::default();320 let mut context = KeyContext::default();
293// context.add_identifier("a");321 context.add("a");
294// context.add_identifier("b");322 context.set("c", "d");
295// assert!(predicate.eval(&[context]));323 assert!(predicate.eval(&[context]));
296 324
297// let mut context = ActionContext::default();325 let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
298// context.add_identifier("a");326 assert!(predicate.eval(&[KeyContext::default()]));
299// context.add_key("c", "x");327 }
300// assert!(!predicate.eval(&[context]));328
301 329 #[test]
302// let mut context = ActionContext::default();330 fn test_context_child_predicate_eval() {
303// context.add_identifier("a");331 let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap();
304// context.add_key("c", "d");332 let contexts = [
305// assert!(predicate.eval(&[context]));333 context_set(&["a", "b"]),
306 334 context_set(&["c", "d"]), // match this context
307// let predicate = KeymapContextPredicate::parse("!a").unwrap();335 context_set(&["e", "f"]),
308// assert!(predicate.eval(&[ActionContext::default()]));336 ];
309// }337
310 338 assert!(!predicate.eval(&contexts[..=0]));
311// #[test]339 assert!(predicate.eval(&contexts[..=1]));
312// fn test_context_child_predicate_eval() {340 assert!(!predicate.eval(&contexts[..=2]));
313// let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();341
314// let contexts = [342 let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap();
315// context_set(&["e", "f"]),343 let contexts = [
316// context_set(&["c", "d"]), // match this context344 context_set(&["a", "b"]),
317// context_set(&["a", "b"]),345 context_set(&["c", "d"]),
318// ];346 context_set(&["e"]),
319 347 context_set(&["a", "b"]),
320// assert!(!predicate.eval(&contexts[0..]));348 context_set(&["c"]),
321// assert!(predicate.eval(&contexts[1..]));349 context_set(&["e"]), // only match this context
322// assert!(!predicate.eval(&contexts[2..]));350 context_set(&["f"]),
323 351 ];
324// let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();352
325// let contexts = [353 assert!(!predicate.eval(&contexts[..=0]));
326// context_set(&["f"]),354 assert!(!predicate.eval(&contexts[..=1]));
327// context_set(&["e"]), // only match this context355 assert!(!predicate.eval(&contexts[..=2]));
328// context_set(&["c"]),356 assert!(!predicate.eval(&contexts[..=3]));
329// context_set(&["a", "b"]),357 assert!(!predicate.eval(&contexts[..=4]));
330// context_set(&["e"]),358 assert!(predicate.eval(&contexts[..=5]));
331// context_set(&["c", "d"]),359 assert!(!predicate.eval(&contexts[..=6]));
332// context_set(&["a", "b"]),360
333// ];361 fn context_set(names: &[&str]) -> KeyContext {
334 362 let mut keymap = KeyContext::default();
335// assert!(!predicate.eval(&contexts[0..]));363 names.iter().for_each(|name| keymap.add(name.to_string()));
336// assert!(predicate.eval(&contexts[1..]));364 keymap
337// assert!(!predicate.eval(&contexts[2..]));365 }
338// assert!(!predicate.eval(&contexts[3..]));366 }
339// assert!(!predicate.eval(&contexts[4..]));367
340// assert!(!predicate.eval(&contexts[5..]));368 #[test]
341// assert!(!predicate.eval(&contexts[6..]));369 fn test_matcher() {
342 370 #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
343// fn context_set(names: &[&str]) -> ActionContext {371 pub struct A(pub String);
344// let mut keymap = ActionContext::new();372 impl_actions!(test, [A]);
345// names373 actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
346// .iter()374
347// .for_each(|name| keymap.add_identifier(name.to_string()));375 #[derive(Clone, Debug, Eq, PartialEq)]
348// keymap376 struct ActionArg {
349// }377 a: &'static str,
350// }378 }
351 379
352// #[test]380 let keymap = Keymap::new(vec![
353// fn test_matcher() -> Result<()> {381 KeyBinding::new("a", A("x".to_string()), Some("a")),
354// #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]382 KeyBinding::new("b", B, Some("a")),
355// pub struct A(pub String);383 KeyBinding::new("a b", Ab, Some("a || b")),
356// impl_actions!(test, [A]);384 KeyBinding::new("$", Dollar, Some("a")),
357// actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);385 KeyBinding::new("\"", Quote, Some("a")),
358 386 KeyBinding::new("alt-s", Ess, Some("a")),
359// #[derive(Clone, Debug, Eq, PartialEq)]387 KeyBinding::new("ctrl-`", Backtick, Some("a")),
360// struct ActionArg {388 ]);
361// a: &'static str,389
362// }390 let mut context_a = KeyContext::default();
363 391 context_a.add("a");
364// let keymap = Keymap::new(vec![392
365// Binding::new("a", A("x".to_string()), Some("a")),393 let mut context_b = KeyContext::default();
366// Binding::new("b", B, Some("a")),394 context_b.add("b");
367// Binding::new("a b", Ab, Some("a || b")),395
368// Binding::new("$", Dollar, Some("a")),396 let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
369// Binding::new("\"", Quote, Some("a")),397
370// Binding::new("alt-s", Ess, Some("a")),398 // Basic match
371// Binding::new("ctrl-`", Backtick, Some("a")),399 assert_eq!(
372// ]);400 matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
373 401 KeyMatch::Some(vec![Box::new(A("x".to_string()))])
374// let mut context_a = ActionContext::default();402 );
375// context_a.add_identifier("a");403 matcher.clear_pending();
376 404
377// let mut context_b = ActionContext::default();405 // Multi-keystroke match
378// context_b.add_identifier("b");406 assert_eq!(
379 407 matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]),
380// let mut matcher = KeymapMatcher::new(keymap);408 KeyMatch::Pending
381 409 );
382// // Basic match410 assert_eq!(
383// assert_eq!(411 matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
384// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),412 KeyMatch::Some(vec![Box::new(Ab)])
385// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])413 );
386// );414 matcher.clear_pending();
387// matcher.clear_pending();415
388 416 // Failed matches don't interfere with matching subsequent keys
389// // Multi-keystroke match417 assert_eq!(
390// assert_eq!(418 matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]),
391// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),419 KeyMatch::None
392// KeyMatch::Pending420 );
393// );421 assert_eq!(
394// assert_eq!(422 matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
395// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),423 KeyMatch::Some(vec![Box::new(A("x".to_string()))])
396// KeyMatch::Matches(vec![(1, Box::new(Ab))])424 );
397// );425 matcher.clear_pending();
398// matcher.clear_pending();426
399 427 let mut context_c = KeyContext::default();
400// // Failed matches don't interfere with matching subsequent keys428 context_c.add("c");
401// assert_eq!(429
402// matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),430 assert_eq!(
403// KeyMatch::None431 matcher.match_keystroke(
404// );432 &Keystroke::parse("a").unwrap(),
405// assert_eq!(433 &[context_c.clone(), context_b.clone()]
406// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),434 ),
407// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])435 KeyMatch::Pending
408// );436 );
409// matcher.clear_pending();437 assert_eq!(
410 438 matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
411// // Pending keystrokes are cleared when the context changes439 KeyMatch::Some(vec![Box::new(Ab)])
412// assert_eq!(440 );
413// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),441
414// KeyMatch::Pending442 // handle Czech $ (option + 4 key)
415// );443 assert_eq!(
416// assert_eq!(444 matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]),
417// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),445 KeyMatch::Some(vec![Box::new(Dollar)])
418// KeyMatch::None446 );
419// );447
420// matcher.clear_pending();448 // handle Brazillian quote (quote key then space key)
421 449 assert_eq!(
422// let mut context_c = ActionContext::default();450 matcher.match_keystroke(
423// context_c.add_identifier("c");451 &Keystroke::parse("space->\"").unwrap(),
424 452 &[context_a.clone()]
425// // Pending keystrokes are maintained per-view453 ),
426// assert_eq!(454 KeyMatch::Some(vec![Box::new(Quote)])
427// matcher.match_keystroke(455 );
428// Keystroke::parse("a")?,456
429// vec![(1, context_b.clone()), (2, context_c.clone())]457 // handle ctrl+` on a brazillian keyboard
430// ),458 assert_eq!(
431// KeyMatch::Pending459 matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
432// );460 KeyMatch::Some(vec![Box::new(Backtick)])
433// assert_eq!(461 );
434// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),462
435// KeyMatch::Matches(vec![(1, Box::new(Ab))])463 // handle alt-s on a US keyboard
436// );464 assert_eq!(
437 465 matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]),
438// // handle Czech $ (option + 4 key)466 KeyMatch::Some(vec![Box::new(Ess)])
439// assert_eq!(467 );
440// matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),468 }
441// KeyMatch::Matches(vec![(1, Box::new(Dollar))])469}
442// );
443
444// // handle Brazillian quote (quote key then space key)
445// assert_eq!(
446// matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
447// KeyMatch::Matches(vec![(1, Box::new(Quote))])
448// );
449
450// // handle ctrl+` on a brazillian keyboard
451// assert_eq!(
452// matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
453// KeyMatch::Matches(vec![(1, Box::new(Backtick))])
454// );
455
456// // handle alt-s on a US keyboard
457// assert_eq!(
458// matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
459// KeyMatch::Matches(vec![(1, Box::new(Ess))])
460// );
461
462// Ok(())
463// }
464// }
crates/gpui/src/platform/mac/platform.rs+47−19
@@ -978,8 +978,12 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { · 977 unmodified lines
978 unsafe {978 unsafe {
979 if let Some(event) = InputEvent::from_native(native_event, None) {979 if let Some(event) = InputEvent::from_native(native_event, None) {
980 let platform = get_mac_platform(this);980 let platform = get_mac_platform(this);
981 if let Some(callback) = platform.0.lock().event.as_mut() {981 let mut lock = platform.0.lock();
982 if !callback(event) {982 if let Some(mut callback) = lock.event.take() {
983 drop(lock);
984 let result = callback(event);
985 platform.0.lock().event.get_or_insert(callback);
986 if !result {
983 return;987 return;
984 }988 }
985 }989 }
@@ -1004,30 +1008,42 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { · 18 unmodified lines
1004extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {1008extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
1005 if !has_open_windows {1009 if !has_open_windows {
1006 let platform = unsafe { get_mac_platform(this) };1010 let platform = unsafe { get_mac_platform(this) };
1007 if let Some(callback) = platform.0.lock().reopen.as_mut() {1011 let mut lock = platform.0.lock();
1012 if let Some(mut callback) = lock.reopen.take() {
1013 drop(lock);
1008 callback();1014 callback();
1015 platform.0.lock().reopen.get_or_insert(callback);
1009 }1016 }
1010 }1017 }
1011}1018}
1012 1019
1013extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {1020extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
1014 let platform = unsafe { get_mac_platform(this) };1021 let platform = unsafe { get_mac_platform(this) };
1015 if let Some(callback) = platform.0.lock().become_active.as_mut() {1022 let mut lock = platform.0.lock();
1023 if let Some(mut callback) = lock.become_active.take() {
1024 drop(lock);
1016 callback();1025 callback();
1026 platform.0.lock().become_active.get_or_insert(callback);
1017 }1027 }
1018}1028}
1019 1029
1020extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {1030extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
1021 let platform = unsafe { get_mac_platform(this) };1031 let platform = unsafe { get_mac_platform(this) };
1022 if let Some(callback) = platform.0.lock().resign_active.as_mut() {1032 let mut lock = platform.0.lock();
1033 if let Some(mut callback) = lock.resign_active.take() {
1034 drop(lock);
1023 callback();1035 callback();
1036 platform.0.lock().resign_active.get_or_insert(callback);
1024 }1037 }
1025}1038}
1026 1039
1027extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {1040extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
1028 let platform = unsafe { get_mac_platform(this) };1041 let platform = unsafe { get_mac_platform(this) };
1029 if let Some(callback) = platform.0.lock().quit.as_mut() {1042 let mut lock = platform.0.lock();
1043 if let Some(mut callback) = lock.quit.take() {
1044 drop(lock);
1030 callback();1045 callback();
1046 platform.0.lock().quit.get_or_insert(callback);
1031 }1047 }
1032}1048}
1033 1049
@@ -1047,22 +1063,27 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { · 13 unmodified lines
1047 .collect::<Vec<_>>()1063 .collect::<Vec<_>>()
1048 };1064 };
1049 let platform = unsafe { get_mac_platform(this) };1065 let platform = unsafe { get_mac_platform(this) };
1050 if let Some(callback) = platform.0.lock().open_urls.as_mut() {1066 let mut lock = platform.0.lock();
1067 if let Some(mut callback) = lock.open_urls.take() {
1068 drop(lock);
1051 callback(urls);1069 callback(urls);
1070 platform.0.lock().open_urls.get_or_insert(callback);
1052 }1071 }
1053}1072}
1054 1073
1055extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {1074extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
1056 unsafe {1075 unsafe {
1057 let platform = get_mac_platform(this);1076 let platform = get_mac_platform(this);
1058 let mut platform = platform.0.lock();1077 let mut lock = platform.0.lock();
1059 if let Some(mut callback) = platform.menu_command.take() {1078 if let Some(mut callback) = lock.menu_command.take() {
1060 let tag: NSInteger = msg_send![item, tag];1079 let tag: NSInteger = msg_send![item, tag];
1061 let index = tag as usize;1080 let index = tag as usize;
1062 if let Some(action) = platform.menu_actions.get(index) {1081 if let Some(action) = lock.menu_actions.get(index) {
1063 callback(action.as_ref());1082 let action = action.boxed_clone();
1083 drop(lock);
1084 callback(&*action);
1064 }1085 }
1065 platform.menu_command = Some(callback);1086 platform.0.lock().menu_command.get_or_insert(callback);
1066 }1087 }
1067 }1088 }
1068}1089}
@@ -1071,14 +1092,20 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { · 2 unmodified lines
1071 unsafe {1092 unsafe {
1072 let mut result = false;1093 let mut result = false;
1073 let platform = get_mac_platform(this);1094 let platform = get_mac_platform(this);
1074 let mut platform = platform.0.lock();1095 let mut lock = platform.0.lock();
1075 if let Some(mut callback) = platform.validate_menu_command.take() {1096 if let Some(mut callback) = lock.validate_menu_command.take() {
1076 let tag: NSInteger = msg_send![item, tag];1097 let tag: NSInteger = msg_send![item, tag];
1077 let index = tag as usize;1098 let index = tag as usize;
1078 if let Some(action) = platform.menu_actions.get(index) {1099 if let Some(action) = lock.menu_actions.get(index) {
1100 let action = action.boxed_clone();
1101 drop(lock);
1079 result = callback(action.as_ref());1102 result = callback(action.as_ref());
1080 }1103 }
1081 platform.validate_menu_command = Some(callback);1104 platform
1105 .0
1106 .lock()
1107 .validate_menu_command
1108 .get_or_insert(callback);
1082 }1109 }
1083 result1110 result
1084 }1111 }
@@ -1087,10 +1114,11 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { · 2 unmodified lines
1087extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {1114extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
1088 unsafe {1115 unsafe {
1089 let platform = get_mac_platform(this);1116 let platform = get_mac_platform(this);
1090 let mut platform = platform.0.lock();1117 let mut lock = platform.0.lock();
1091 if let Some(mut callback) = platform.will_open_menu.take() {1118 if let Some(mut callback) = lock.will_open_menu.take() {
1119 drop(lock);
1092 callback();1120 callback();
1093 platform.will_open_menu = Some(callback);1121 platform.0.lock().will_open_menu.get_or_insert(callback);
1094 }1122 }
1095 }1123 }
1096}1124}
crates/gpui/src/platform/mac/text_system.rs+49−166
@@ -190,6 +190,9 @@ impl MacTextSystemState { · 189 unmodified lines
190 for font in family.fonts() {190 for font in family.fonts() {
191 let mut font = font.load()?;191 let mut font = font.load()?;
192 open_type::apply_features(&mut font, features);192 open_type::apply_features(&mut font, features);
193 let Some(_) = font.glyph_for_char('m') else {
194 continue;
195 };
193 let font_id = FontId(self.fonts.len());196 let font_id = FontId(self.fonts.len());
194 font_ids.push(font_id);197 font_ids.push(font_id);
195 let postscript_name = font.postscript_name().unwrap();198 let postscript_name = font.postscript_name().unwrap();
@@ -592,169 +595,49 @@ impl From<FontStyle> for FontkitStyle { · 396 unmodified lines
592 }595 }
593}596}
594 597
595// #[cfg(test)]598#[cfg(test)]
596// mod tests {599mod tests {
597// use super::*;600 use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
598// use crate::AppContext;601
599// use font_kit::properties::{Style, Weight};602 #[test]
600// use platform::FontSystem as _;603 fn test_wrap_line() {
601 604 let fonts = MacTextSystem::new();
602// #[crate::test(self, retries = 5)]605 let font_id = fonts.font_id(&font("Helvetica")).unwrap();
603// fn test_layout_str(_: &mut AppContext) {606
604// // This is failing intermittently on CI and we don't have time to figure it out607 let line = "one two three four five\n";
605// let fonts = FontSystem::new();608 let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
606// let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();609 assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
607// let menlo_regular = RunStyle {610
608// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),611 let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
609// color: Default::default(),612 let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
610// underline: Default::default(),613 assert_eq!(
611// };614 wrap_boundaries,
612// let menlo_italic = RunStyle {615 &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
613// font_id: fonts616 );
614// .select_font(&menlo, Properties::new().style(Style::Italic))617 }
615// .unwrap(),618
616// color: Default::default(),619 #[test]
617// underline: Default::default(),620 fn test_layout_line_bom_char() {
618// };621 let fonts = MacTextSystem::new();
619// let menlo_bold = RunStyle {622 let font_id = fonts.font_id(&font("Helvetica")).unwrap();
620// font_id: fonts623 let line = "\u{feff}";
621// .select_font(&menlo, Properties::new().weight(Weight::BOLD))624 let mut style = FontRun {
622// .unwrap(),625 font_id,
623// color: Default::default(),626 len: line.len(),
624// underline: Default::default(),627 };
625// };628
626// assert_ne!(menlo_regular, menlo_italic);629 let layout = fonts.layout_line(line, px(16.), &[style]);
627// assert_ne!(menlo_regular, menlo_bold);630 assert_eq!(layout.len, line.len());
628// assert_ne!(menlo_italic, menlo_bold);631 assert!(layout.runs.is_empty());
629 632
630// let line = fonts.layout_line(633 let line = "a\u{feff}b";
631// "hello world",634 style.len = line.len();
632// 16.0,635 let layout = fonts.layout_line(line, px(16.), &[style]);
633// &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],636 assert_eq!(layout.len, line.len());
634// );637 assert_eq!(layout.runs.len(), 1);
635// assert_eq!(line.runs.len(), 3);638 assert_eq!(layout.runs[0].glyphs.len(), 2);
636// assert_eq!(line.runs[0].font_id, menlo_bold.font_id);639 assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a
637// assert_eq!(line.runs[0].glyphs.len(), 2);640 // There's no glyph for \u{feff}
638// assert_eq!(line.runs[1].font_id, menlo_italic.font_id);641 assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b
639// assert_eq!(line.runs[1].glyphs.len(), 4);642 }
640// assert_eq!(line.runs[2].font_id, menlo_regular.font_id);643}
641// assert_eq!(line.runs[2].glyphs.len(), 5);
642// }
643
644// #[test]
645// fn test_glyph_offsets() -> crate::Result<()> {
646// let fonts = FontSystem::new();
647// let zapfino = fonts.load_family("Zapfino", &Default::default())?;
648// let zapfino_regular = RunStyle {
649// font_id: fonts.select_font(&zapfino, &Properties::new())?,
650// color: Default::default(),
651// underline: Default::default(),
652// };
653// let menlo = fonts.load_family("Menlo", &Default::default())?;
654// let menlo_regular = RunStyle {
655// font_id: fonts.select_font(&menlo, &Properties::new())?,
656// color: Default::default(),
657// underline: Default::default(),
658// };
659
660// let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
661// let line = fonts.layout_line(
662// text,
663// 16.0,
664// &[
665// (9, zapfino_regular),
666// (13, menlo_regular),
667// (text.len() - 22, zapfino_regular),
668// ],
669// );
670// assert_eq!(
671// line.runs
672// .iter()
673// .flat_map(|r| r.glyphs.iter())
674// .map(|g| g.index)
675// .collect::<Vec<_>>(),
676// vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
677// );
678// Ok(())
679// }
680
681// #[test]
682// #[ignore]
683// fn test_rasterize_glyph() {
684// use std::{fs::File, io::BufWriter, path::Path};
685
686// let fonts = FontSystem::new();
687// let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
688// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
689// let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
690
691// const VARIANTS: usize = 1;
692// for i in 0..VARIANTS {
693// let variant = i as f32 / VARIANTS as f32;
694// let (bounds, bytes) = fonts
695// .rasterize_glyph(
696// font_id,
697// 16.0,
698// glyph_id,
699// vec2f(variant, variant),
700// 2.,
701// RasterizationOptions::Alpha,
702// )
703// .unwrap();
704
705// let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
706// let path = Path::new(&name);
707// let file = File::create(path).unwrap();
708// let w = &mut BufWriter::new(file);
709
710// let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
711// encoder.set_color(png::ColorType::Grayscale);
712// encoder.set_depth(png::BitDepth::Eight);
713// let mut writer = encoder.write_header().unwrap();
714// writer.write_image_data(&bytes).unwrap();
715// }
716// }
717
718// #[test]
719// fn test_wrap_line() {
720// let fonts = FontSystem::new();
721// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
722// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
723
724// let line = "one two three four five\n";
725// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
726// assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
727
728// let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
729// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
730// assert_eq!(
731// wrap_boundaries,
732// &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
733// );
734// }
735
736// #[test]
737// fn test_layout_line_bom_char() {
738// let fonts = FontSystem::new();
739// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
740// let style = RunStyle {
741// font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
742// color: Default::default(),
743// underline: Default::default(),
744// };
745
746// let line = "\u{feff}";
747// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
748// assert_eq!(layout.len, line.len());
749// assert!(layout.runs.is_empty());
750
751// let line = "a\u{feff}b";
752// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
753// assert_eq!(layout.len, line.len());
754// assert_eq!(layout.runs.len(), 1);
755// assert_eq!(layout.runs[0].glyphs.len(), 2);
756// assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
757// // There's no glyph for \u{feff}
758// assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
759// }
760// }
crates/gpui/src/platform/mac/window.rs+5−34
@@ -268,6 +268,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C · 267 unmodified lines
268 sel!(windowShouldClose:),268 sel!(windowShouldClose:),
269 window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,269 window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
270 );270 );
271
271 decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));272 decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
272 273
273 decl.add_method(274 decl.add_method(
@@ -683,9 +684,6 @@ impl Drop for MacWindow { · 409 unmodified lines
683 this.executor684 this.executor
684 .spawn(async move {685 .spawn(async move {
685 unsafe {686 unsafe {
686 // todo!() this panic()s when you click the red close button
687 // unless should_close returns false.
688 // (luckliy in zed it always returns false)
689 window.close();687 window.close();
690 }688 }
691 })689 })
@@ -1104,37 +1102,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: · 412 unmodified lines
1104 .flatten()1102 .flatten()
1105 .is_some();1103 .is_some();
1106 if !is_composing {1104 if !is_composing {
1107 // if the IME has changed the key, we'll first emit an event with the character1105 handled = callback(InputEvent::KeyDown(event));
1108 // generated by the IME system; then fallback to the keystroke if that is not
1109 // handled.
1110 // cases that we have working:
1111 // - " on a brazillian layout by typing <quote><space>
1112 // - ctrl-` on a brazillian layout by typing <ctrl-`>
1113 // - $ on a czech QWERTY layout by typing <alt-4>
1114 // - 4 on a czech QWERTY layout by typing <shift-4>
1115 // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
1116 if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
1117 let event_with_ime_text = KeyDownEvent {
1118 is_held: false,
1119 keystroke: Keystroke {
1120 // we match ctrl because some use-cases need it.
1121 // we don't match alt because it's often used to generate the optional character
1122 // we don't match shift because we're not here with letters (usually)
1123 // we don't match cmd/fn because they don't seem to use IME
1124 modifiers: Default::default(),
1125 key: ime_text.clone().unwrap(),
1126 ime_key: None, // todo!("handle IME key")
1127 },
1128 };
1129 handled = callback(InputEvent::KeyDown(event_with_ime_text));
1130 }
1131 if !handled {
1132 // empty key happens when you type a deadkey in input composition.
1133 // (e.g. on a brazillian keyboard typing quote is a deadkey)
1134 if !event.keystroke.key.is_empty() {
1135 handled = callback(InputEvent::KeyDown(event));
1136 }
1137 }
1138 }1106 }
1139 1107
1140 if !handled {1108 if !handled {
@@ -1574,6 +1542,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS · 433 unmodified lines
1574 replacement_range,1542 replacement_range,
1575 text: text.to_string(),1543 text: text.to_string(),
1576 });1544 });
1545 if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
1546 pending_key_down.0.keystroke.ime_key = Some(text.to_string());
1547 }
1577 window_state.lock().pending_key_down = Some(pending_key_down);1548 window_state.lock().pending_key_down = Some(pending_key_down);
1578 }1549 }
1579 }1550 }
crates/gpui/src/text_system.rs+10−15
@@ -65,6 +65,9 @@ impl TextSystem { · 64 unmodified lines
65 }65 }
66 }66 }
67 67
68 pub fn all_font_families(&self) -> Vec<String> {
69 self.platform_text_system.all_font_families()
70 }
68 pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {71 pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
69 self.platform_text_system.add_fonts(fonts)72 self.platform_text_system.add_fonts(fonts)
70 }73 }
@@ -368,28 +371,20 @@ impl TextSystem { · 297 unmodified lines
368 self.line_layout_cache.finish_frame(reused_views)371 self.line_layout_cache.finish_frame(reused_views)
369 }372 }
370 373
371 pub fn line_wrapper(374 pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
372 self: &Arc<Self>,
373 font: Font,
374 font_size: Pixels,
375 ) -> Result<LineWrapperHandle> {
376 let lock = &mut self.wrapper_pool.lock();375 let lock = &mut self.wrapper_pool.lock();
377 let font_id = self.font_id(&font)?;376 let font_id = self.resolve_font(&font);
378 let wrappers = lock377 let wrappers = lock
379 .entry(FontIdWithSize { font_id, font_size })378 .entry(FontIdWithSize { font_id, font_size })
380 .or_default();379 .or_default();
381 let wrapper = wrappers.pop().map(anyhow::Ok).unwrap_or_else(|| {380 let wrapper = wrappers.pop().unwrap_or_else(|| {
382 Ok(LineWrapper::new(381 LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
383 font_id,382 });
384 font_size,
385 self.platform_text_system.clone(),
386 ))
387 })?;
388 383
389 Ok(LineWrapperHandle {384 LineWrapperHandle {
390 wrapper: Some(wrapper),385 wrapper: Some(wrapper),
391 text_system: self.clone(),386 text_system: self.clone(),
392 })387 }
393 }388 }
394 389
395 pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {390 pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
crates/gpui/src/text_system/line_wrapper.rs+63−68
@@ -137,7 +137,7 @@ impl Boundary { · 136 unmodified lines
137#[cfg(test)]137#[cfg(test)]
138mod tests {138mod tests {
139 use super::*;139 use super::*;
140 use crate::{font, TestAppContext, TestDispatcher};140 use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
141 use rand::prelude::*;141 use rand::prelude::*;
142 142
143 #[test]143 #[test]
@@ -206,75 +206,70 @@ mod tests { · 62 unmodified lines
206 });206 });
207 }207 }
208 208
209 // todo!("move this to a test on TextSystem::layout_text")209 // For compatibility with the test macro
210 // todo! repeat this test210 use crate as gpui;
211 // #[test]
212 // fn test_wrap_shaped_line() {
213 // App::test().run(|cx| {
214 // let text_system = cx.text_system().clone();
215 211
216 // let normal = TextRun {212 #[crate::test]
217 // len: 0,213 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
218 // font: font("Helvetica"),214 cx.update(|cx| {
219 // color: Default::default(),215 let text_system = cx.text_system().clone();
220 // underline: Default::default(),216
221 // };217 let normal = TextRun {
222 // let bold = TextRun {218 len: 0,
223 // len: 0,219 font: font("Helvetica"),
224 // font: font("Helvetica").bold(),220 color: Default::default(),
225 // color: Default::default(),221 underline: Default::default(),
226 // underline: Default::default(),222 background_color: None,
227 // };223 };
224 let bold = TextRun {
225 len: 0,
226 font: font("Helvetica").bold(),
227 color: Default::default(),
228 underline: Default::default(),
229 background_color: None,
230 };
228 231
229 // impl TextRun {232 impl TextRun {
230 // fn with_len(&self, len: usize) -> Self {233 fn with_len(&self, len: usize) -> Self {
231 // let mut this = self.clone();234 let mut this = self.clone();
232 // this.len = len;235 this.len = len;
233 // this236 this
234 // }237 }
235 // }238 }
236 239
237 // let text = "aa bbb cccc ddddd eeee".into();240 let text = "aa bbb cccc ddddd eeee".into();
238 // let lines = text_system241 let lines = text_system
239 // .layout_text(242 .shape_text(
240 // &text,243 text,
241 // px(16.),244 px(16.),
242 // &[245 &[
243 // normal.with_len(4),246 normal.with_len(4),
244 // bold.with_len(5),247 bold.with_len(5),
245 // normal.with_len(6),248 normal.with_len(6),
246 // bold.with_len(1),249 bold.with_len(1),
247 // normal.with_len(7),250 normal.with_len(7),
248 // ],251 ],
249 // None,252 Some(px(72.)),
250 // )253 )
251 // .unwrap();254 .unwrap();
252 // let line = &lines[0];
253 255
254 // let mut wrapper = LineWrapper::new(256 assert_eq!(
255 // text_system.font_id(&normal.font).unwrap(),257 lines[0].layout.wrap_boundaries(),
256 // px(16.),258 &[
257 // text_system.platform_text_system.clone(),259 WrapBoundary {
258 // );260 run_ix: 1,
259 // assert_eq!(261 glyph_ix: 3
260 // wrapper262 },
261 // .wrap_shaped_line(&text, &line, px(72.))263 WrapBoundary {
262 // .collect::<Vec<_>>(),264 run_ix: 2,
263 // &[265 glyph_ix: 3
264 // ShapedBoundary {266 },
265 // run_ix: 1,267 WrapBoundary {
266 // glyph_ix: 3268 run_ix: 4,
267 // },269 glyph_ix: 2
268 // ShapedBoundary {270 }
269 // run_ix: 2,271 ],
270 // glyph_ix: 3272 );
271 // },273 });
272 // ShapedBoundary {274 }
273 // run_ix: 4,
274 // glyph_ix: 2
275 // }
276 // ],
277 // );
278 // });
279 // }
280}275}
crates/gpui/src/window.rs+13−4
@@ -1545,9 +1545,6 @@ impl<'a> WindowContext<'a> { · 1544 unmodified lines
1545 .finish(&mut self.window.rendered_frame);1545 .finish(&mut self.window.rendered_frame);
1546 ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());1546 ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
1547 1547
1548 self.window.refreshing = false;
1549 self.window.drawing = false;
1550
1551 let previous_focus_path = self.window.rendered_frame.focus_path();1548 let previous_focus_path = self.window.rendered_frame.focus_path();
1552 let previous_window_active = self.window.rendered_frame.window_active;1549 let previous_window_active = self.window.rendered_frame.window_active;
1553 mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);1550 mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
@@ -1586,6 +1583,8 @@ impl<'a> WindowContext<'a> { · 32 unmodified lines
1586 self.window1583 self.window
1587 .platform_window1584 .platform_window
1588 .draw(&self.window.rendered_frame.scene);1585 .draw(&self.window.rendered_frame.scene);
1586 self.window.refreshing = false;
1587 self.window.drawing = false;
1589 }1588 }
1590 1589
1591 /// Dispatch a mouse or keyboard event on the window.1590 /// Dispatch a mouse or keyboard event on the window.
@@ -2158,7 +2157,17 @@ impl<'a> WindowContext<'a> { · 566 unmodified lines
2158 let mut this = self.to_async();2157 let mut this = self.to_async();
2159 self.window2158 self.window
2160 .platform_window2159 .platform_window
2161 .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true)))2160 .on_should_close(Box::new(move || {
2161 this.update(|_, cx| {
2162 // Ensure that the window is removed from the app if it's been closed
2163 // by always pre-empting the system close event.
2164 if f(cx) {
2165 cx.remove_window();
2166 }
2167 false
2168 })
2169 .unwrap_or(true)
2170 }))
2162 }2171 }
2163}2172}
2164 2173
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift+47−3
@@ -12,6 +12,8 @@ class LKRoomDelegate: RoomDelegate { · 11 unmodified lines
12 var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void12 var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
13 var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void13 var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
14 var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void14 var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
15 var onDidPublishOrUnpublishLocalAudioTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
16 var onDidPublishOrUnpublishLocalVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
15 17
16 init(18 init(
17 data: UnsafeRawPointer,19 data: UnsafeRawPointer,
@@ -21,7 +23,10 @@ class LKRoomDelegate: RoomDelegate { · 3 unmodified lines
21 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,23 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
22 onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,24 onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
23 onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,25 onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
24 onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)26 onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
27 onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
28 onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
29 )
25 {30 {
26 self.data = data31 self.data = data
27 self.onDidDisconnect = onDidDisconnect32 self.onDidDisconnect = onDidDisconnect
@@ -31,6 +36,8 @@ class LKRoomDelegate: RoomDelegate { · 3 unmodified lines
31 self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack36 self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
32 self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack37 self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
33 self.onActiveSpeakersChanged = onActiveSpeakersChanged38 self.onActiveSpeakersChanged = onActiveSpeakersChanged
39 self.onDidPublishOrUnpublishLocalAudioTrack = onDidPublishOrUnpublishLocalAudioTrack
40 self.onDidPublishOrUnpublishLocalVideoTrack = onDidPublishOrUnpublishLocalVideoTrack
34 }41 }
35 42
36 func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {43 func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
@@ -65,6 +72,22 @@ class LKRoomDelegate: RoomDelegate { · 28 unmodified lines
65 self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)72 self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
66 }73 }
67 }74 }
75
76 func room(_ room: Room, localParticipant: LocalParticipant, didPublish publication: LocalTrackPublication) {
77 if publication.kind == .video {
78 self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
79 } else if publication.kind == .audio {
80 self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
81 }
82 }
83
84 func room(_ room: Room, localParticipant: LocalParticipant, didUnpublish publication: LocalTrackPublication) {
85 if publication.kind == .video {
86 self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
87 } else if publication.kind == .audio {
88 self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
89 }
90 }
68}91}
69 92
70class LKVideoRenderer: NSObject, VideoRenderer {93class LKVideoRenderer: NSObject, VideoRenderer {
@@ -109,7 +132,9 @@ public func LKRoomDelegateCreate( · 38 unmodified lines
109 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,132 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
110 onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,133 onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
111 onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,134 onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
112 onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void135 onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
136 onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
137 onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
113) -> UnsafeMutableRawPointer {138) -> UnsafeMutableRawPointer {
114 let delegate = LKRoomDelegate(139 let delegate = LKRoomDelegate(
115 data: data,140 data: data,
@@ -119,7 +144,9 @@ public func LKRoomDelegateCreate( · 3 unmodified lines
119 onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,144 onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
120 onActiveSpeakersChanged: onActiveSpeakerChanged,145 onActiveSpeakersChanged: onActiveSpeakerChanged,
121 onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,146 onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
122 onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack147 onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack,
148 onDidPublishOrUnpublishLocalAudioTrack: onDidPublishOrUnpublishLocalAudioTrack,
149 onDidPublishOrUnpublishLocalVideoTrack: onDidPublishOrUnpublishLocalVideoTrack
123 )150 )
124 return Unmanaged.passRetained(delegate).toOpaque()151 return Unmanaged.passRetained(delegate).toOpaque()
125}152}
@@ -292,6 +319,14 @@ public func LKLocalTrackPublicationSetMute( · 166 unmodified lines
292 }319 }
293}320}
294 321
322@_cdecl("LKLocalTrackPublicationIsMuted")
323public func LKLocalTrackPublicationIsMuted(
324 publication: UnsafeRawPointer
325) -> Bool {
326 let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
327 return publication.muted
328}
329
295@_cdecl("LKRemoteTrackPublicationSetEnabled")330@_cdecl("LKRemoteTrackPublicationSetEnabled")
296public func LKRemoteTrackPublicationSetEnabled(331public func LKRemoteTrackPublicationSetEnabled(
297 publication: UnsafeRawPointer,332 publication: UnsafeRawPointer,
@@ -325,3 +360,12 @@ public func LKRemoteTrackPublicationGetSid( · 27 unmodified lines
325 360
326 return publication.sid as CFString361 return publication.sid as CFString
327}362}
363
364@_cdecl("LKLocalTrackPublicationGetSid")
365public func LKLocalTrackPublicationGetSid(
366 publication: UnsafeRawPointer
367) -> CFString {
368 let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
369
370 return publication.sid as CFString
371}
crates/live_kit_client/examples/test_app.rs+16−19
@@ -2,9 +2,7 @@ use std::{sync::Arc, time::Duration}; · 1 unmodified line
2 2
3use futures::StreamExt;3use futures::StreamExt;
4use gpui::{actions, KeyBinding, Menu, MenuItem};4use gpui::{actions, KeyBinding, Menu, MenuItem};
5use live_kit_client::{5use live_kit_client::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate};
6 LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
7};
8use live_kit_server::token::{self, VideoGrant};6use live_kit_server::token::{self, VideoGrant};
9use log::LevelFilter;7use log::LevelFilter;
10use simplelog::SimpleLogger;8use simplelog::SimpleLogger;
@@ -60,12 +58,12 @@ fn main() { · 49 unmodified lines
60 let room_b = Room::new();58 let room_b = Room::new();
61 room_b.connect(&live_kit_url, &user2_token).await.unwrap();59 room_b.connect(&live_kit_url, &user2_token).await.unwrap();
62 60
63 let mut audio_track_updates = room_b.remote_audio_track_updates();61 let mut room_updates = room_b.updates();
64 let audio_track = LocalAudioTrack::create();62 let audio_track = LocalAudioTrack::create();
65 let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap();63 let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap();
66 64
67 if let RemoteAudioTrackUpdate::Subscribed(track, _) =65 if let RoomUpdate::SubscribedToRemoteAudioTrack(track, _) =
68 audio_track_updates.next().await.unwrap()66 room_updates.next().await.unwrap()
69 {67 {
70 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");68 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
71 assert_eq!(remote_tracks.len(), 1);69 assert_eq!(remote_tracks.len(), 1);
@@ -78,8 +76,8 @@ fn main() { · 6 unmodified lines
78 audio_track_publication.set_mute(true).await.unwrap();76 audio_track_publication.set_mute(true).await.unwrap();
79 77
80 println!("waiting for mute changed!");78 println!("waiting for mute changed!");
81 if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =79 if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
82 audio_track_updates.next().await.unwrap()80 room_updates.next().await.unwrap()
83 {81 {
84 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");82 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
85 assert_eq!(remote_tracks[0].sid(), track_id);83 assert_eq!(remote_tracks[0].sid(), track_id);
@@ -90,8 +88,8 @@ fn main() { · 4 unmodified lines
90 88
91 audio_track_publication.set_mute(false).await.unwrap();89 audio_track_publication.set_mute(false).await.unwrap();
92 90
93 if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =91 if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
94 audio_track_updates.next().await.unwrap()92 room_updates.next().await.unwrap()
95 {93 {
96 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");94 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
97 assert_eq!(remote_tracks[0].sid(), track_id);95 assert_eq!(remote_tracks[0].sid(), track_id);
@@ -110,13 +108,13 @@ fn main() { · 12 unmodified lines
110 room_a.unpublish_track(audio_track_publication);108 room_a.unpublish_track(audio_track_publication);
111 109
112 // Clear out any active speakers changed messages110 // Clear out any active speakers changed messages
113 let mut next = audio_track_updates.next().await.unwrap();111 let mut next = room_updates.next().await.unwrap();
114 while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next {112 while let RoomUpdate::ActiveSpeakersChanged { speakers } = next {
115 println!("Speakers changed: {:?}", speakers);113 println!("Speakers changed: {:?}", speakers);
116 next = audio_track_updates.next().await.unwrap();114 next = room_updates.next().await.unwrap();
117 }115 }
118 116
119 if let RemoteAudioTrackUpdate::Unsubscribed {117 if let RoomUpdate::UnsubscribedFromRemoteAudioTrack {
120 publisher_id,118 publisher_id,
121 track_id,119 track_id,
122 } = next120 } = next
@@ -128,7 +126,6 @@ fn main() { · 5 unmodified lines
128 panic!("unexpected message");126 panic!("unexpected message");
129 }127 }
130 128
131 let mut video_track_updates = room_b.remote_video_track_updates();
132 let displays = room_a.display_sources().await.unwrap();129 let displays = room_a.display_sources().await.unwrap();
133 let display = displays.into_iter().next().unwrap();130 let display = displays.into_iter().next().unwrap();
134 131
@@ -136,8 +133,8 @@ fn main() { · 1 unmodified line
136 let local_video_track_publication =133 let local_video_track_publication =
137 room_a.publish_video_track(local_video_track).await.unwrap();134 room_a.publish_video_track(local_video_track).await.unwrap();
138 135
139 if let RemoteVideoTrackUpdate::Subscribed(track) =136 if let RoomUpdate::SubscribedToRemoteVideoTrack(track) =
140 video_track_updates.next().await.unwrap()137 room_updates.next().await.unwrap()
141 {138 {
142 let remote_video_tracks = room_b.remote_video_tracks("test-participant-1");139 let remote_video_tracks = room_b.remote_video_tracks("test-participant-1");
143 assert_eq!(remote_video_tracks.len(), 1);140 assert_eq!(remote_video_tracks.len(), 1);
@@ -152,10 +149,10 @@ fn main() { · 8 unmodified lines
152 .pop()149 .pop()
153 .unwrap();150 .unwrap();
154 room_a.unpublish_track(local_video_track_publication);151 room_a.unpublish_track(local_video_track_publication);
155 if let RemoteVideoTrackUpdate::Unsubscribed {152 if let RoomUpdate::UnsubscribedFromRemoteVideoTrack {
156 publisher_id,153 publisher_id,
157 track_id,154 track_id,
158 } = video_track_updates.next().await.unwrap()155 } = room_updates.next().await.unwrap()
159 {156 {
160 assert_eq!(publisher_id, "test-participant-1");157 assert_eq!(publisher_id, "test-participant-1");
161 assert_eq!(remote_video_track.sid(), track_id);158 assert_eq!(remote_video_track.sid(), track_id);
crates/live_kit_client/src/live_kit_client.rs+24−0
@@ -1,3 +1,5 @@
1use std::sync::Arc;
2
1#[cfg(not(any(test, feature = "test-support")))]3#[cfg(not(any(test, feature = "test-support")))]
2pub mod prod;4pub mod prod;
3 5
@@ -9,3 +11,25 @@ pub mod test; · 5 unmodified lines
9 11
10#[cfg(any(test, feature = "test-support"))]12#[cfg(any(test, feature = "test-support"))]
11pub use test::*;13pub use test::*;
14
15pub type Sid = String;
16
17#[derive(Clone, Eq, PartialEq)]
18pub enum ConnectionState {
19 Disconnected,
20 Connected { url: String, token: String },
21}
22
23#[derive(Clone)]
24pub enum RoomUpdate {
25 ActiveSpeakersChanged { speakers: Vec<Sid> },
26 RemoteAudioTrackMuteChanged { track_id: Sid, muted: bool },
27 SubscribedToRemoteVideoTrack(Arc<RemoteVideoTrack>),
28 SubscribedToRemoteAudioTrack(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
29 UnsubscribedFromRemoteVideoTrack { publisher_id: Sid, track_id: Sid },
30 UnsubscribedFromRemoteAudioTrack { publisher_id: Sid, track_id: Sid },
31 LocalAudioTrackPublished { publication: LocalTrackPublication },
32 LocalAudioTrackUnpublished { publication: LocalTrackPublication },
33 LocalVideoTrackPublished { publication: LocalTrackPublication },
34 LocalVideoTrackUnpublished { publication: LocalTrackPublication },
35}
crates/live_kit_client/src/prod.rs+97−52
@@ -1,3 +1,4 @@
1use crate::{ConnectionState, RoomUpdate, Sid};
1use anyhow::{anyhow, Context, Result};2use anyhow::{anyhow, Context, Result};
2use core_foundation::{3use core_foundation::{
3 array::{CFArray, CFArrayRef},4 array::{CFArray, CFArrayRef},
@@ -76,6 +77,16 @@ extern "C" { · 72 unmodified lines
76 publisher_id: CFStringRef,77 publisher_id: CFStringRef,
77 track_id: CFStringRef,78 track_id: CFStringRef,
78 ),79 ),
80 on_did_publish_or_unpublish_local_audio_track: extern "C" fn(
81 callback_data: *mut c_void,
82 publication: swift::LocalTrackPublication,
83 is_published: bool,
84 ),
85 on_did_publish_or_unpublish_local_video_track: extern "C" fn(
86 callback_data: *mut c_void,
87 publication: swift::LocalTrackPublication,
88 is_published: bool,
89 ),
79 ) -> swift::RoomDelegate;90 ) -> swift::RoomDelegate;
80 91
81 fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room;92 fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room;
@@ -151,26 +162,19 @@ extern "C" { · 69 unmodified lines
151 callback_data: *mut c_void,162 callback_data: *mut c_void,
152 );163 );
153 164
165 fn LKLocalTrackPublicationIsMuted(publication: swift::LocalTrackPublication) -> bool;
154 fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool;166 fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool;
167 fn LKLocalTrackPublicationGetSid(publication: swift::LocalTrackPublication) -> CFStringRef;
155 fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef;168 fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef;
156}169}
157 170
158pub type Sid = String;
159
160#[derive(Clone, Eq, PartialEq)]
161pub enum ConnectionState {
162 Disconnected,
163 Connected { url: String, token: String },
164}
165
166pub struct Room {171pub struct Room {
167 native_room: swift::Room,172 native_room: swift::Room,
168 connection: Mutex<(173 connection: Mutex<(
169 watch::Sender<ConnectionState>,174 watch::Sender<ConnectionState>,
170 watch::Receiver<ConnectionState>,175 watch::Receiver<ConnectionState>,
171 )>,176 )>,
172 remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,177 update_subscribers: Mutex<Vec<mpsc::UnboundedSender<RoomUpdate>>>,
173 remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
174 _delegate: RoomDelegate,178 _delegate: RoomDelegate,
175}179}
176 180
@@ -181,8 +185,7 @@ impl Room { · 4 unmodified lines
181 Self {185 Self {
182 native_room: unsafe { LKRoomCreate(delegate.native_delegate) },186 native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
183 connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),187 connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
184 remote_audio_track_subscribers: Default::default(),188 update_subscribers: Default::default(),
185 remote_video_track_subscribers: Default::default(),
186 _delegate: delegate,189 _delegate: delegate,
187 }190 }
188 })191 })
@@ -397,15 +400,9 @@ impl Room { · 208 unmodified lines
397 }400 }
398 }401 }
399 402
400 pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {403 pub fn updates(&self) -> mpsc::UnboundedReceiver<RoomUpdate> {
401 let (tx, rx) = mpsc::unbounded();404 let (tx, rx) = mpsc::unbounded();
402 self.remote_audio_track_subscribers.lock().push(tx);405 self.update_subscribers.lock().push(tx);
403 rx
404 }
405
406 pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackUpdate> {
407 let (tx, rx) = mpsc::unbounded();
408 self.remote_video_track_subscribers.lock().push(tx);
409 rx406 rx
410 }407 }
411 408
@@ -416,8 +413,8 @@ impl Room { · 4 unmodified lines
416 ) {413 ) {
417 let track = Arc::new(track);414 let track = Arc::new(track);
418 let publication = Arc::new(publication);415 let publication = Arc::new(publication);
419 self.remote_audio_track_subscribers.lock().retain(|tx| {416 self.update_subscribers.lock().retain(|tx| {
420 tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(417 tx.unbounded_send(RoomUpdate::SubscribedToRemoteAudioTrack(
421 track.clone(),418 track.clone(),
422 publication.clone(),419 publication.clone(),
423 ))420 ))
@@ -426,8 +423,8 @@ impl Room { · 2 unmodified lines
426 }423 }
427 424
428 fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {425 fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {
429 self.remote_audio_track_subscribers.lock().retain(|tx| {426 self.update_subscribers.lock().retain(|tx| {
430 tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed {427 tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteAudioTrack {
431 publisher_id: publisher_id.clone(),428 publisher_id: publisher_id.clone(),
432 track_id: track_id.clone(),429 track_id: track_id.clone(),
433 })430 })
@@ -436,8 +433,8 @@ impl Room { · 2 unmodified lines
436 }433 }
437 434
438 fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {435 fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
439 self.remote_audio_track_subscribers.lock().retain(|tx| {436 self.update_subscribers.lock().retain(|tx| {
440 tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged {437 tx.unbounded_send(RoomUpdate::RemoteAudioTrackMuteChanged {
441 track_id: track_id.clone(),438 track_id: track_id.clone(),
442 muted,439 muted,
443 })440 })
@@ -445,29 +442,26 @@ impl Room { · 1 unmodified line
445 });442 });
446 }443 }
447 444
448 // A vec of publisher IDs
449 fn active_speakers_changed(&self, speakers: Vec<String>) {445 fn active_speakers_changed(&self, speakers: Vec<String>) {
450 self.remote_audio_track_subscribers446 self.update_subscribers.lock().retain(move |tx| {
451 .lock()447 tx.unbounded_send(RoomUpdate::ActiveSpeakersChanged {
452 .retain(move |tx| {448 speakers: speakers.clone(),
453 tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged {449 })
454 speakers: speakers.clone(),450 .is_ok()
455 })451 });
456 .is_ok()
457 });
458 }452 }
459 453
460 fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {454 fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
461 let track = Arc::new(track);455 let track = Arc::new(track);
462 self.remote_video_track_subscribers.lock().retain(|tx| {456 self.update_subscribers.lock().retain(|tx| {
463 tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone()))457 tx.unbounded_send(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
464 .is_ok()458 .is_ok()
465 });459 });
466 }460 }
467 461
468 fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {462 fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {
469 self.remote_video_track_subscribers.lock().retain(|tx| {463 self.update_subscribers.lock().retain(|tx| {
470 tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed {464 tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteVideoTrack {
471 publisher_id: publisher_id.clone(),465 publisher_id: publisher_id.clone(),
472 track_id: track_id.clone(),466 track_id: track_id.clone(),
473 })467 })
@@ -529,6 +523,8 @@ impl RoomDelegate { · 55 unmodified lines
529 Self::on_active_speakers_changed,523 Self::on_active_speakers_changed,
530 Self::on_did_subscribe_to_remote_video_track,524 Self::on_did_subscribe_to_remote_video_track,
531 Self::on_did_unsubscribe_from_remote_video_track,525 Self::on_did_unsubscribe_from_remote_video_track,
526 Self::on_did_publish_or_unpublish_local_audio_track,
527 Self::on_did_publish_or_unpublish_local_video_track,
532 )528 )
533 };529 };
534 Self {530 Self {
@@ -642,6 +638,46 @@ impl RoomDelegate { · 107 unmodified lines
642 }638 }
643 let _ = Weak::into_raw(room);639 let _ = Weak::into_raw(room);
644 }640 }
641
642 extern "C" fn on_did_publish_or_unpublish_local_audio_track(
643 room: *mut c_void,
644 publication: swift::LocalTrackPublication,
645 is_published: bool,
646 ) {
647 let room = unsafe { Weak::from_raw(room as *mut Room) };
648 if let Some(room) = room.upgrade() {
649 let publication = LocalTrackPublication::new(publication);
650 let update = if is_published {
651 RoomUpdate::LocalAudioTrackPublished { publication }
652 } else {
653 RoomUpdate::LocalAudioTrackUnpublished { publication }
654 };
655 room.update_subscribers
656 .lock()
657 .retain(|tx| tx.unbounded_send(update.clone()).is_ok());
658 }
659 let _ = Weak::into_raw(room);
660 }
661
662 extern "C" fn on_did_publish_or_unpublish_local_video_track(
663 room: *mut c_void,
664 publication: swift::LocalTrackPublication,
665 is_published: bool,
666 ) {
667 let room = unsafe { Weak::from_raw(room as *mut Room) };
668 if let Some(room) = room.upgrade() {
669 let publication = LocalTrackPublication::new(publication);
670 let update = if is_published {
671 RoomUpdate::LocalVideoTrackPublished { publication }
672 } else {
673 RoomUpdate::LocalVideoTrackUnpublished { publication }
674 };
675 room.update_subscribers
676 .lock()
677 .retain(|tx| tx.unbounded_send(update.clone()).is_ok());
678 }
679 let _ = Weak::into_raw(room);
680 }
645}681}
646 682
647impl Drop for RoomDelegate {683impl Drop for RoomDelegate {
@@ -691,6 +727,10 @@ impl LocalTrackPublication { · 43 unmodified lines
691 Self(native_track_publication)727 Self(native_track_publication)
692 }728 }
693 729
730 pub fn sid(&self) -> String {
731 unsafe { CFString::wrap_under_get_rule(LKLocalTrackPublicationGetSid(self.0)).to_string() }
732 }
733
694 pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {734 pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
695 let (tx, rx) = futures::channel::oneshot::channel();735 let (tx, rx) = futures::channel::oneshot::channel();
696 736
@@ -715,6 +755,19 @@ impl LocalTrackPublication { · 18 unmodified lines
715 755
716 async move { rx.await.unwrap() }756 async move { rx.await.unwrap() }
717 }757 }
758
759 pub fn is_muted(&self) -> bool {
760 unsafe { LKLocalTrackPublicationIsMuted(self.0) }
761 }
762}
763
764impl Clone for LocalTrackPublication {
765 fn clone(&self) -> Self {
766 unsafe {
767 CFRetain(self.0 .0);
768 }
769 Self(self.0)
770 }
718}771}
719 772
720impl Drop for LocalTrackPublication {773impl Drop for LocalTrackPublication {
@@ -811,7 +864,11 @@ impl RemoteAudioTrack { · 90 unmodified lines
811 864
812impl Drop for RemoteAudioTrack {865impl Drop for RemoteAudioTrack {
813 fn drop(&mut self) {866 fn drop(&mut self) {
814 unsafe { CFRelease(self.native_track.0) }867 // todo: uncomment this `CFRelease`, unless we find that it was causing
868 // the crash in the `livekit.multicast` thread.
869 //
870 // unsafe { CFRelease(self.native_track.0) }
871 let _ = self.native_track;
815 }872 }
816}873}
817 874
@@ -889,18 +946,6 @@ impl Drop for RemoteVideoTrack { · 71 unmodified lines
889 }946 }
890}947}
891 948
892pub enum RemoteVideoTrackUpdate {
893 Subscribed(Arc<RemoteVideoTrack>),
894 Unsubscribed { publisher_id: Sid, track_id: Sid },
895}
896
897pub enum RemoteAudioTrackUpdate {
898 ActiveSpeakersChanged { speakers: Vec<Sid> },
899 MuteChanged { track_id: Sid, muted: bool },
900 Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
901 Unsubscribed { publisher_id: Sid, track_id: Sid },
902}
903
904pub struct MacOSDisplay(swift::MacOSDisplay);949pub struct MacOSDisplay(swift::MacOSDisplay);
905 950
906impl MacOSDisplay {951impl MacOSDisplay {
crates/live_kit_client/src/test.rs+65−61
@@ -1,3 +1,4 @@
1use crate::{ConnectionState, RoomUpdate, Sid};
1use anyhow::{anyhow, Context, Result};2use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;3use async_trait::async_trait;
3use collections::{BTreeMap, HashMap};4use collections::{BTreeMap, HashMap};
@@ -7,7 +8,14 @@ use live_kit_server::{proto, token}; · 3 unmodified lines
7use media::core_video::CVImageBuffer;8use media::core_video::CVImageBuffer;
8use parking_lot::Mutex;9use parking_lot::Mutex;
9use postage::watch;10use postage::watch;
10use std::{future::Future, mem, sync::Arc};11use std::{
12 future::Future,
13 mem,
14 sync::{
15 atomic::{AtomicBool, Ordering::SeqCst},
16 Arc,
17 },
18};
11 19
12static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());20static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
13 21
@@ -104,9 +112,8 @@ impl TestServer { · 90 unmodified lines
104 client_room112 client_room
105 .0113 .0
106 .lock()114 .lock()
107 .video_track_updates115 .updates_tx
108 .0116 .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
109 .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
110 .unwrap();117 .unwrap();
111 }118 }
112 room.client_rooms.insert(identity, client_room);119 room.client_rooms.insert(identity, client_room);
@@ -176,7 +183,11 @@ impl TestServer { · 63 unmodified lines
176 }183 }
177 }184 }
178 185
179 async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> {186 async fn publish_video_track(
187 &self,
188 token: String,
189 local_track: LocalVideoTrack,
190 ) -> Result<Sid> {
180 self.executor.simulate_random_delay().await;191 self.executor.simulate_random_delay().await;
181 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;192 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
182 let identity = claims.sub.unwrap().to_string();193 let identity = claims.sub.unwrap().to_string();
@@ -198,8 +209,9 @@ impl TestServer { · 15 unmodified lines
198 return Err(anyhow!("user is not allowed to publish"));209 return Err(anyhow!("user is not allowed to publish"));
199 }210 }
200 211
212 let sid = nanoid::nanoid!(17);
201 let track = Arc::new(RemoteVideoTrack {213 let track = Arc::new(RemoteVideoTrack {
202 sid: nanoid::nanoid!(17),214 sid: sid.clone(),
203 publisher_id: identity.clone(),215 publisher_id: identity.clone(),
204 frames_rx: local_track.frames_rx.clone(),216 frames_rx: local_track.frames_rx.clone(),
205 });217 });
@@ -211,21 +223,20 @@ impl TestServer { · 5 unmodified lines
211 let _ = client_room223 let _ = client_room
212 .0224 .0
213 .lock()225 .lock()
214 .video_track_updates226 .updates_tx
215 .0227 .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
216 .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
217 .unwrap();228 .unwrap();
218 }229 }
219 }230 }
220 231
221 Ok(())232 Ok(sid)
222 }233 }
223 234
224 async fn publish_audio_track(235 async fn publish_audio_track(
225 &self,236 &self,
226 token: String,237 token: String,
227 _local_track: &LocalAudioTrack,238 _local_track: &LocalAudioTrack,
228 ) -> Result<()> {239 ) -> Result<Sid> {
229 self.executor.simulate_random_delay().await;240 self.executor.simulate_random_delay().await;
230 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;241 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
231 let identity = claims.sub.unwrap().to_string();242 let identity = claims.sub.unwrap().to_string();
@@ -247,8 +258,9 @@ impl TestServer { · 15 unmodified lines
247 return Err(anyhow!("user is not allowed to publish"));258 return Err(anyhow!("user is not allowed to publish"));
248 }259 }
249 260
261 let sid = nanoid::nanoid!(17);
250 let track = Arc::new(RemoteAudioTrack {262 let track = Arc::new(RemoteAudioTrack {
251 sid: nanoid::nanoid!(17),263 sid: sid.clone(),
252 publisher_id: identity.clone(),264 publisher_id: identity.clone(),
253 });265 });
254 266
@@ -261,9 +273,8 @@ impl TestServer { · 6 unmodified lines
261 let _ = client_room273 let _ = client_room
262 .0274 .0
263 .lock()275 .lock()
264 .audio_track_updates276 .updates_tx
265 .0277 .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
266 .try_broadcast(RemoteAudioTrackUpdate::Subscribed(
267 track.clone(),278 track.clone(),
268 publication.clone(),279 publication.clone(),
269 ))280 ))
@@ -271,7 +282,7 @@ impl TestServer { · 1 unmodified line
271 }282 }
272 }283 }
273 284
274 Ok(())285 Ok(sid)
275 }286 }
276 287
277 fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {288 fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
@@ -369,39 +380,26 @@ impl live_kit_server::api::Client for TestApiClient { · 91 unmodified lines
369 }380 }
370}381}
371 382
372pub type Sid = String;
373
374struct RoomState {383struct RoomState {
375 connection: (384 connection: (
376 watch::Sender<ConnectionState>,385 watch::Sender<ConnectionState>,
377 watch::Receiver<ConnectionState>,386 watch::Receiver<ConnectionState>,
378 ),387 ),
379 display_sources: Vec<MacOSDisplay>,388 display_sources: Vec<MacOSDisplay>,
380 audio_track_updates: (389 updates_tx: async_broadcast::Sender<RoomUpdate>,
381 async_broadcast::Sender<RemoteAudioTrackUpdate>,390 updates_rx: async_broadcast::Receiver<RoomUpdate>,
382 async_broadcast::Receiver<RemoteAudioTrackUpdate>,
383 ),
384 video_track_updates: (
385 async_broadcast::Sender<RemoteVideoTrackUpdate>,
386 async_broadcast::Receiver<RemoteVideoTrackUpdate>,
387 ),
388}
389
390#[derive(Clone, Eq, PartialEq)]
391pub enum ConnectionState {
392 Disconnected,
393 Connected { url: String, token: String },
394}391}
395 392
396pub struct Room(Mutex<RoomState>);393pub struct Room(Mutex<RoomState>);
397 394
398impl Room {395impl Room {
399 pub fn new() -> Arc<Self> {396 pub fn new() -> Arc<Self> {
397 let (updates_tx, updates_rx) = async_broadcast::broadcast(128);
400 Arc::new(Self(Mutex::new(RoomState {398 Arc::new(Self(Mutex::new(RoomState {
401 connection: watch::channel_with(ConnectionState::Disconnected),399 connection: watch::channel_with(ConnectionState::Disconnected),
402 display_sources: Default::default(),400 display_sources: Default::default(),
403 video_track_updates: async_broadcast::broadcast(128),401 updates_tx,
404 audio_track_updates: async_broadcast::broadcast(128),402 updates_rx,
405 })))403 })))
406 }404 }
407 405
@@ -440,10 +438,14 @@ impl Room { · 32 unmodified lines
440 let this = self.clone();438 let this = self.clone();
441 let track = track.clone();439 let track = track.clone();
442 async move {440 async move {
443 this.test_server()441 let sid = this
442 .test_server()
444 .publish_video_track(this.token(), track)443 .publish_video_track(this.token(), track)
445 .await?;444 .await?;
446 Ok(LocalTrackPublication)445 Ok(LocalTrackPublication {
446 muted: Default::default(),
447 sid,
448 })
447 }449 }
448 }450 }
449 pub fn publish_audio_track(451 pub fn publish_audio_track(
@@ -453,10 +455,14 @@ impl Room { · 3 unmodified lines
453 let this = self.clone();455 let this = self.clone();
454 let track = track.clone();456 let track = track.clone();
455 async move {457 async move {
456 this.test_server()458 let sid = this
459 .test_server()
457 .publish_audio_track(this.token(), &track)460 .publish_audio_track(this.token(), &track)
458 .await?;461 .await?;
459 Ok(LocalTrackPublication)462 Ok(LocalTrackPublication {
463 muted: Default::default(),
464 sid,
465 })
460 }466 }
461 }467 }
462 468
@@ -505,12 +511,8 @@ impl Room { · 42 unmodified lines
505 .collect()511 .collect()
506 }512 }
507 513
508 pub fn remote_audio_track_updates(&self) -> impl Stream<Item = RemoteAudioTrackUpdate> {514 pub fn updates(&self) -> impl Stream<Item = RoomUpdate> {
509 self.0.lock().audio_track_updates.1.clone()515 self.0.lock().updates_rx.clone()
510 }
511
512 pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
513 self.0.lock().video_track_updates.1.clone()
514 }516 }
515 517
516 pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {518 pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {
@@ -555,11 +557,27 @@ impl Drop for Room { · 38 unmodified lines
555 }557 }
556}558}
557 559
558pub struct LocalTrackPublication;560#[derive(Clone)]
561pub struct LocalTrackPublication {
562 sid: String,
563 muted: Arc<AtomicBool>,
564}
559 565
560impl LocalTrackPublication {566impl LocalTrackPublication {
561 pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {567 pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
562 async { Ok(()) }568 let muted = self.muted.clone();
569 async move {
570 muted.store(mute, SeqCst);
571 Ok(())
572 }
573 }
574
575 pub fn is_muted(&self) -> bool {
576 self.muted.load(SeqCst)
577 }
578
579 pub fn sid(&self) -> String {
580 self.sid.clone()
563 }581 }
564}582}
565 583
@@ -646,20 +664,6 @@ impl RemoteAudioTrack { · 80 unmodified lines
646 }664 }
647}665}
648 666
649#[derive(Clone)]
650pub enum RemoteVideoTrackUpdate {
651 Subscribed(Arc<RemoteVideoTrack>),
652 Unsubscribed { publisher_id: Sid, track_id: Sid },
653}
654
655#[derive(Clone)]
656pub enum RemoteAudioTrackUpdate {
657 ActiveSpeakersChanged { speakers: Vec<Sid> },
658 MuteChanged { track_id: Sid, muted: bool },
659 Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
660 Unsubscribed { publisher_id: Sid, track_id: Sid },
661}
662
663#[derive(Clone)]667#[derive(Clone)]
664pub struct MacOSDisplay {668pub struct MacOSDisplay {
665 frames: (669 frames: (
crates/project/src/project.rs+43−39
@@ -130,7 +130,7 @@ pub struct Project { · 129 unmodified lines
130 next_diagnostic_group_id: usize,130 next_diagnostic_group_id: usize,
131 user_store: Model<UserStore>,131 user_store: Model<UserStore>,
132 fs: Arc<dyn Fs>,132 fs: Arc<dyn Fs>,
133 client_state: Option<ProjectClientState>,133 client_state: ProjectClientState,
134 collaborators: HashMap<proto::PeerId, Collaborator>,134 collaborators: HashMap<proto::PeerId, Collaborator>,
135 client_subscriptions: Vec<client::Subscription>,135 client_subscriptions: Vec<client::Subscription>,
136 _subscriptions: Vec<gpui::Subscription>,136 _subscriptions: Vec<gpui::Subscription>,
@@ -254,8 +254,10 @@ enum WorktreeHandle { · 117 unmodified lines
254 Weak(WeakModel<Worktree>),254 Weak(WeakModel<Worktree>),
255}255}
256 256
257#[derive(Debug)]
257enum ProjectClientState {258enum ProjectClientState {
258 Local {259 Local,
260 Shared {
259 remote_id: u64,261 remote_id: u64,
260 updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,262 updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,
261 _send_updates: Task<Result<()>>,263 _send_updates: Task<Result<()>>,
@@ -657,7 +659,7 @@ impl Project { · 395 unmodified lines
657 local_buffer_ids_by_entry_id: Default::default(),659 local_buffer_ids_by_entry_id: Default::default(),
658 buffer_snapshots: Default::default(),660 buffer_snapshots: Default::default(),
659 join_project_response_message_id: 0,661 join_project_response_message_id: 0,
660 client_state: None,662 client_state: ProjectClientState::Local,
661 opened_buffer: watch::channel(),663 opened_buffer: watch::channel(),
662 client_subscriptions: Vec::new(),664 client_subscriptions: Vec::new(),
663 _subscriptions: vec![665 _subscriptions: vec![
@@ -756,12 +758,12 @@ impl Project { · 92 unmodified lines
756 cx.on_app_quit(Self::shutdown_language_servers),758 cx.on_app_quit(Self::shutdown_language_servers),
757 ],759 ],
758 client: client.clone(),760 client: client.clone(),
759 client_state: Some(ProjectClientState::Remote {761 client_state: ProjectClientState::Remote {
760 sharing_has_stopped: false,762 sharing_has_stopped: false,
761 capability: Capability::ReadWrite,763 capability: Capability::ReadWrite,
762 remote_id,764 remote_id,
763 replica_id,765 replica_id,
764 }),766 },
765 supplementary_language_servers: HashMap::default(),767 supplementary_language_servers: HashMap::default(),
766 language_servers: Default::default(),768 language_servers: Default::default(),
767 language_server_ids: Default::default(),769 language_server_ids: Default::default(),
@@ -828,16 +830,16 @@ impl Project { · 60 unmodified lines
828 830
829 fn release(&mut self, cx: &mut AppContext) {831 fn release(&mut self, cx: &mut AppContext) {
830 match &self.client_state {832 match &self.client_state {
831 Some(ProjectClientState::Local { .. }) => {833 ProjectClientState::Local => {}
834 ProjectClientState::Shared { .. } => {
832 let _ = self.unshare_internal(cx);835 let _ = self.unshare_internal(cx);
833 }836 }
834 Some(ProjectClientState::Remote { remote_id, .. }) => {837 ProjectClientState::Remote { remote_id, .. } => {
835 let _ = self.client.send(proto::LeaveProject {838 let _ = self.client.send(proto::LeaveProject {
836 project_id: *remote_id,839 project_id: *remote_id,
837 });840 });
838 self.disconnected_from_host_internal(cx);841 self.disconnected_from_host_internal(cx);
839 }842 }
840 _ => {}
841 }843 }
842 }844 }
843 845
@@ -1058,21 +1060,22 @@ impl Project { · 214 unmodified lines
1058 }1060 }
1059 1061
1060 pub fn remote_id(&self) -> Option<u64> {1062 pub fn remote_id(&self) -> Option<u64> {
1061 match self.client_state.as_ref()? {1063 match self.client_state {
1062 ProjectClientState::Local { remote_id, .. }1064 ProjectClientState::Local => None,
1063 | ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),1065 ProjectClientState::Shared { remote_id, .. }
1066 | ProjectClientState::Remote { remote_id, .. } => Some(remote_id),
1064 }1067 }
1065 }1068 }
1066 1069
1067 pub fn replica_id(&self) -> ReplicaId {1070 pub fn replica_id(&self) -> ReplicaId {
1068 match &self.client_state {1071 match self.client_state {
1069 Some(ProjectClientState::Remote { replica_id, .. }) => *replica_id,1072 ProjectClientState::Remote { replica_id, .. } => replica_id,
1070 _ => 0,1073 _ => 0,
1071 }1074 }
1072 }1075 }
1073 1076
1074 fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {1077 fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
1075 if let Some(ProjectClientState::Local { updates_tx, .. }) = &mut self.client_state {1078 if let ProjectClientState::Shared { updates_tx, .. } = &mut self.client_state {
1076 updates_tx1079 updates_tx
1077 .unbounded_send(LocalProjectUpdate::WorktreesChanged)1080 .unbounded_send(LocalProjectUpdate::WorktreesChanged)
1078 .ok();1081 .ok();
@@ -1362,7 +1365,7 @@ impl Project { · 283 unmodified lines
1362 }1365 }
1363 1366
1364 pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {1367 pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
1365 if self.client_state.is_some() {1368 if !matches!(self.client_state, ProjectClientState::Local) {
1366 return Err(anyhow!("project was already shared"));1369 return Err(anyhow!("project was already shared"));
1367 }1370 }
1368 self.client_subscriptions.push(1371 self.client_subscriptions.push(
@@ -1423,7 +1426,7 @@ impl Project { · 54 unmodified lines
1423 1426
1424 let (updates_tx, mut updates_rx) = mpsc::unbounded();1427 let (updates_tx, mut updates_rx) = mpsc::unbounded();
1425 let client = self.client.clone();1428 let client = self.client.clone();
1426 self.client_state = Some(ProjectClientState::Local {1429 self.client_state = ProjectClientState::Shared {
1427 remote_id: project_id,1430 remote_id: project_id,
1428 updates_tx,1431 updates_tx,
1429 _send_updates: cx.spawn(move |this, mut cx| async move {1432 _send_updates: cx.spawn(move |this, mut cx| async move {
@@ -1508,7 +1511,7 @@ impl Project { · 78 unmodified lines
1508 }1511 }
1509 Ok(())1512 Ok(())
1510 }),1513 }),
1511 });1514 };
1512 1515
1513 self.metadata_changed(cx);1516 self.metadata_changed(cx);
1514 cx.emit(Event::RemoteIdChanged(Some(project_id)));1517 cx.emit(Event::RemoteIdChanged(Some(project_id)));
@@ -1578,7 +1581,8 @@ impl Project { · 63 unmodified lines
1578 return Err(anyhow!("attempted to unshare a remote project"));1581 return Err(anyhow!("attempted to unshare a remote project"));
1579 }1582 }
1580 1583
1581 if let Some(ProjectClientState::Local { remote_id, .. }) = self.client_state.take() {1584 if let ProjectClientState::Shared { remote_id, .. } = self.client_state {
1585 self.client_state = ProjectClientState::Local;
1582 self.collaborators.clear();1586 self.collaborators.clear();
1583 self.shared_buffers.clear();1587 self.shared_buffers.clear();
1584 self.client_subscriptions.clear();1588 self.client_subscriptions.clear();
@@ -1629,23 +1633,23 @@ impl Project { · 44 unmodified lines
1629 } else {1633 } else {
1630 Capability::ReadOnly1634 Capability::ReadOnly
1631 };1635 };
1632 if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {1636 if let ProjectClientState::Remote { capability, .. } = &mut self.client_state {
1633 if *capability == new_capability {1637 if *capability == new_capability {
1634 return;1638 return;
1635 }1639 }
1636 1640
1637 *capability = new_capability;1641 *capability = new_capability;
1638 }1642 for buffer in self.opened_buffers() {
1639 for buffer in self.opened_buffers() {1643 buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));
1640 buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));1644 }
1641 }1645 }
1642 }1646 }
1643 1647
1644 fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {1648 fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
1645 if let Some(ProjectClientState::Remote {1649 if let ProjectClientState::Remote {
1646 sharing_has_stopped,1650 sharing_has_stopped,
1647 ..1651 ..
1648 }) = &mut self.client_state1652 } = &mut self.client_state
1649 {1653 {
1650 *sharing_has_stopped = true;1654 *sharing_has_stopped = true;
1651 1655
@@ -1684,18 +1688,18 @@ impl Project { · 32 unmodified lines
1684 1688
1685 pub fn is_disconnected(&self) -> bool {1689 pub fn is_disconnected(&self) -> bool {
1686 match &self.client_state {1690 match &self.client_state {
1687 Some(ProjectClientState::Remote {1691 ProjectClientState::Remote {
1688 sharing_has_stopped,1692 sharing_has_stopped,
1689 ..1693 ..
1690 }) => *sharing_has_stopped,1694 } => *sharing_has_stopped,
1691 _ => false,1695 _ => false,
1692 }1696 }
1693 }1697 }
1694 1698
1695 pub fn capability(&self) -> Capability {1699 pub fn capability(&self) -> Capability {
1696 match &self.client_state {1700 match &self.client_state {
1697 Some(ProjectClientState::Remote { capability, .. }) => *capability,1701 ProjectClientState::Remote { capability, .. } => *capability,
1698 Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite,1702 ProjectClientState::Shared { .. } | ProjectClientState::Local => Capability::ReadWrite,
1699 }1703 }
1700 }1704 }
1701 1705
@@ -1705,8 +1709,8 @@ impl Project { · 3 unmodified lines
1705 1709
1706 pub fn is_local(&self) -> bool {1710 pub fn is_local(&self) -> bool {
1707 match &self.client_state {1711 match &self.client_state {
1708 Some(ProjectClientState::Remote { .. }) => false,1712 ProjectClientState::Local | ProjectClientState::Shared { .. } => true,
1709 _ => true,1713 ProjectClientState::Remote { .. } => false,
1710 }1714 }
1711 }1715 }
1712 1716
@@ -6165,8 +6169,8 @@ impl Project { · 4452 unmodified lines
6165 6169
6166 pub fn is_shared(&self) -> bool {6170 pub fn is_shared(&self) -> bool {
6167 match &self.client_state {6171 match &self.client_state {
6168 Some(ProjectClientState::Local { .. }) => true,6172 ProjectClientState::Shared { .. } => true,
6169 _ => false,6173 ProjectClientState::Local | ProjectClientState::Remote { .. } => false,
6170 }6174 }
6171 }6175 }
6172 6176
@@ -7954,7 +7958,7 @@ impl Project { · 1781 unmodified lines
7954 cx: &mut AppContext,7958 cx: &mut AppContext,
7955 ) -> u64 {7959 ) -> u64 {
7956 let buffer_id = buffer.read(cx).remote_id();7960 let buffer_id = buffer.read(cx).remote_id();
7957 if let Some(ProjectClientState::Local { updates_tx, .. }) = &self.client_state {7961 if let ProjectClientState::Shared { updates_tx, .. } = &self.client_state {
7958 updates_tx7962 updates_tx
7959 .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id })7963 .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id })
7960 .ok();7964 .ok();
@@ -8003,21 +8007,21 @@ impl Project { · 42 unmodified lines
8003 }8007 }
8004 8008
8005 fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {8009 fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
8006 let project_id = match self.client_state.as_ref() {8010 let project_id = match self.client_state {
8007 Some(ProjectClientState::Remote {8011 ProjectClientState::Remote {
8008 sharing_has_stopped,8012 sharing_has_stopped,
8009 remote_id,8013 remote_id,
8010 ..8014 ..
8011 }) => {8015 } => {
8012 if *sharing_has_stopped {8016 if sharing_has_stopped {
8013 return Task::ready(Err(anyhow!(8017 return Task::ready(Err(anyhow!(
8014 "can't synchronize remote buffers on a readonly project"8018 "can't synchronize remote buffers on a readonly project"
8015 )));8019 )));
8016 } else {8020 } else {
8017 *remote_id8021 remote_id
8018 }8022 }
8019 }8023 }
8020 Some(ProjectClientState::Local { .. }) | None => {8024 ProjectClientState::Shared { .. } | ProjectClientState::Local => {
8021 return Task::ready(Err(anyhow!(8025 return Task::ready(Err(anyhow!(
8022 "can't synchronize remote buffers on a local project"8026 "can't synchronize remote buffers on a local project"
8023 )))8027 )))
crates/search/src/buffer_search.rs+0−2
@@ -1130,7 +1130,6 @@ mod tests { · 1129 unmodified lines
1130 #[gpui::test]1130 #[gpui::test]
1131 async fn test_search_simple(cx: &mut TestAppContext) {1131 async fn test_search_simple(cx: &mut TestAppContext) {
1132 let (editor, search_bar, cx) = init_test(cx);1132 let (editor, search_bar, cx) = init_test(cx);
1133 // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
1134 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {1133 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1135 background_highlights1134 background_highlights
1136 .into_iter()1135 .into_iter()
@@ -1395,7 +1394,6 @@ mod tests { · 258 unmodified lines
1395 })1394 })
1396 .await1395 .await
1397 .unwrap();1396 .unwrap();
1398 // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1399 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {1397 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1400 background_highlights1398 background_highlights
1401 .into_iter()1399 .into_iter()
crates/search/src/project_search.rs+119−3
@@ -1015,9 +1015,6 @@ impl ProjectSearchView { · 1014 unmodified lines
1015 workspace.add_item(Box::new(view.clone()), cx);1015 workspace.add_item(Box::new(view.clone()), cx);
1016 view1016 view
1017 };1017 };
1018
1019 workspace.add_item(Box::new(search.clone()), cx);
1020
1021 search.update(cx, |search, cx| {1018 search.update(cx, |search, cx| {
1022 if let Some(query) = query {1019 if let Some(query) = query {
1023 search.set_query(&query, cx);1020 search.set_query(&query, cx);
@@ -1984,6 +1981,7 @@ pub mod tests { · 960 unmodified lines
1984 use semantic_index::semantic_index_settings::SemanticIndexSettings;1981 use semantic_index::semantic_index_settings::SemanticIndexSettings;
1985 use serde_json::json;1982 use serde_json::json;
1986 use settings::{Settings, SettingsStore};1983 use settings::{Settings, SettingsStore};
1984 use workspace::DeploySearch;
1987 1985
1988 #[gpui::test]1986 #[gpui::test]
1989 async fn test_project_search(cx: &mut TestAppContext) {1987 async fn test_project_search(cx: &mut TestAppContext) {
@@ -3111,6 +3109,124 @@ pub mod tests { · 1121 unmodified lines
3111 .unwrap();3109 .unwrap();
3112 }3110 }
3113 3111
3112 #[gpui::test]
3113 async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) {
3114 init_test(cx);
3115
3116 let fs = FakeFs::new(cx.background_executor.clone());
3117 fs.insert_tree(
3118 "/dir",
3119 json!({
3120 "one.rs": "const ONE: usize = 1;",
3121 }),
3122 )
3123 .await;
3124 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3125 let worktree_id = project.update(cx, |this, cx| {
3126 this.worktrees().next().unwrap().read(cx).id()
3127 });
3128 let window = cx.add_window(|cx| Workspace::test_new(project, cx));
3129 let panes: Vec<_> = window
3130 .update(cx, |this, _| this.panes().to_owned())
3131 .unwrap();
3132 assert_eq!(panes.len(), 1);
3133 let first_pane = panes.get(0).cloned().unwrap();
3134 assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0);
3135 window
3136 .update(cx, |workspace, cx| {
3137 workspace.open_path(
3138 (worktree_id, "one.rs"),
3139 Some(first_pane.downgrade()),
3140 true,
3141 cx,
3142 )
3143 })
3144 .unwrap()
3145 .await
3146 .unwrap();
3147 assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3148 let second_pane = window
3149 .update(cx, |workspace, cx| {
3150 workspace.split_and_clone(first_pane.clone(), workspace::SplitDirection::Right, cx)
3151 })
3152 .unwrap()
3153 .unwrap();
3154 assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3155 assert!(window
3156 .update(cx, |_, cx| second_pane
3157 .focus_handle(cx)
3158 .contains_focused(cx))
3159 .unwrap());
3160 let search_bar = window.build_view(cx, |_| ProjectSearchBar::new());
3161 window
3162 .update(cx, {
3163 let search_bar = search_bar.clone();
3164 let pane = first_pane.clone();
3165 move |workspace, cx| {
3166 assert_eq!(workspace.panes().len(), 2);
3167 pane.update(cx, move |pane, cx| {
3168 pane.toolbar()
3169 .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx))
3170 });
3171 }
3172 })
3173 .unwrap();
3174 window
3175 .update(cx, {
3176 let search_bar = search_bar.clone();
3177 let pane = second_pane.clone();
3178 move |workspace, cx| {
3179 assert_eq!(workspace.panes().len(), 2);
3180 pane.update(cx, move |pane, cx| {
3181 pane.toolbar()
3182 .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx))
3183 });
3184
3185 ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx)
3186 }
3187 })
3188 .unwrap();
3189
3190 cx.run_until_parked();
3191 assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
3192 assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3193 window
3194 .update(cx, |workspace, cx| {
3195 assert_eq!(workspace.active_pane(), &second_pane);
3196 second_pane.update(cx, |this, cx| {
3197 assert_eq!(this.active_item_index(), 1);
3198 this.activate_prev_item(false, cx);
3199 assert_eq!(this.active_item_index(), 0);
3200 });
3201 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx);
3202 })
3203 .unwrap();
3204 window
3205 .update(cx, |workspace, cx| {
3206 assert_eq!(workspace.active_pane(), &first_pane);
3207 assert_eq!(first_pane.read(cx).items_len(), 1);
3208 assert_eq!(second_pane.read(cx).items_len(), 2);
3209 })
3210 .unwrap();
3211 cx.dispatch_action(window.into(), DeploySearch);
3212
3213 // We should have same # of items in workspace, the only difference being that
3214 // the search we've deployed previously should now be focused.
3215 window
3216 .update(cx, |workspace, cx| {
3217 assert_eq!(workspace.active_pane(), &second_pane);
3218 second_pane.update(cx, |this, _| {
3219 assert_eq!(this.active_item_index(), 1);
3220 assert_eq!(this.items_len(), 2);
3221 });
3222 first_pane.update(cx, |this, cx| {
3223 assert!(!cx.focus_handle().contains_focused(cx));
3224 assert_eq!(this.items_len(), 1);
3225 });
3226 })
3227 .unwrap();
3228 }
3229
3114 pub fn init_test(cx: &mut TestAppContext) {3230 pub fn init_test(cx: &mut TestAppContext) {
3115 cx.update(|cx| {3231 cx.update(|cx| {
3116 let settings = SettingsStore::test(cx);3232 let settings = SettingsStore::test(cx);
crates/terminal_view/src/terminal_element.rs+15−2
@@ -6,7 +6,7 @@ use gpui::{ · 5 unmodified lines
6 InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext,6 InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext,
7 ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point,7 ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point,
8 ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle,8 ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle,
9 WhiteSpace, WindowContext,9 WeakView, WhiteSpace, WindowContext,
10};10};
11use itertools::Itertools;11use itertools::Itertools;
12use language::CursorShape;12use language::CursorShape;
@@ -24,6 +24,7 @@ use terminal::{ · 11 unmodified lines
24};24};
25use theme::{ActiveTheme, Theme, ThemeSettings};25use theme::{ActiveTheme, Theme, ThemeSettings};
26use ui::Tooltip;26use ui::Tooltip;
27use workspace::Workspace;
27 28
28use std::mem;29use std::mem;
29use std::{fmt::Debug, ops::RangeInclusive};30use std::{fmt::Debug, ops::RangeInclusive};
@@ -142,6 +143,7 @@ impl LayoutRect { · 112 unmodified lines
142///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?143///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
143pub struct TerminalElement {144pub struct TerminalElement {
144 terminal: Model<Terminal>,145 terminal: Model<Terminal>,
146 workspace: WeakView<Workspace>,
145 focus: FocusHandle,147 focus: FocusHandle,
146 focused: bool,148 focused: bool,
147 cursor_visible: bool,149 cursor_visible: bool,
@@ -160,6 +162,7 @@ impl StatefulInteractiveElement for TerminalElement {} · 12 unmodified lines
160impl TerminalElement {162impl TerminalElement {
161 pub fn new(163 pub fn new(
162 terminal: Model<Terminal>,164 terminal: Model<Terminal>,
165 workspace: WeakView<Workspace>,
163 focus: FocusHandle,166 focus: FocusHandle,
164 focused: bool,167 focused: bool,
165 cursor_visible: bool,168 cursor_visible: bool,
@@ -167,6 +170,7 @@ impl TerminalElement { · 1 unmodified line
167 ) -> TerminalElement {170 ) -> TerminalElement {
168 TerminalElement {171 TerminalElement {
169 terminal,172 terminal,
173 workspace,
170 focused,174 focused,
171 focus: focus.clone(),175 focus: focus.clone(),
172 cursor_visible,176 cursor_visible,
@@ -762,6 +766,7 @@ impl Element for TerminalElement { · 589 unmodified lines
762 .cursor766 .cursor
763 .as_ref()767 .as_ref()
764 .map(|cursor| cursor.bounding_rect(origin)),768 .map(|cursor| cursor.bounding_rect(origin)),
769 workspace: self.workspace.clone(),
765 };770 };
766 771
767 self.register_mouse_listeners(origin, layout.mode, bounds, cx);772 self.register_mouse_listeners(origin, layout.mode, bounds, cx);
@@ -831,6 +836,7 @@ impl IntoElement for TerminalElement { · 63 unmodified lines
831struct TerminalInputHandler {836struct TerminalInputHandler {
832 cx: AsyncWindowContext,837 cx: AsyncWindowContext,
833 terminal: Model<Terminal>,838 terminal: Model<Terminal>,
839 workspace: WeakView<Workspace>,
834 cursor_bounds: Option<Bounds<Pixels>>,840 cursor_bounds: Option<Bounds<Pixels>>,
835}841}
836 842
@@ -871,7 +877,14 @@ impl PlatformInputHandler for TerminalInputHandler { · 34 unmodified lines
871 .update(|_, cx| {877 .update(|_, cx| {
872 self.terminal.update(cx, |terminal, _| {878 self.terminal.update(cx, |terminal, _| {
873 terminal.input(text.into());879 terminal.input(text.into());
874 })880 });
881
882 self.workspace
883 .update(cx, |this, cx| {
884 let telemetry = this.project().read(cx).client().telemetry().clone();
885 telemetry.log_edit_event("terminal");
886 })
887 .ok();
875 })888 })
876 .ok();889 .ok();
877 }890 }
crates/terminal_view/src/terminal_view.rs+4−0
@@ -73,6 +73,7 @@ pub fn init(cx: &mut AppContext) { · 72 unmodified lines
73///A terminal view, maintains the PTY's file handles and communicates with the terminal73///A terminal view, maintains the PTY's file handles and communicates with the terminal
74pub struct TerminalView {74pub struct TerminalView {
75 terminal: Model<Terminal>,75 terminal: Model<Terminal>,
76 workspace: WeakView<Workspace>,
76 focus_handle: FocusHandle,77 focus_handle: FocusHandle,
77 has_new_content: bool,78 has_new_content: bool,
78 //Currently using iTerm bell, show bell emoji in tab until input is received79 //Currently using iTerm bell, show bell emoji in tab until input is received
@@ -135,6 +136,7 @@ impl TerminalView { · 56 unmodified lines
135 workspace_id: WorkspaceId,136 workspace_id: WorkspaceId,
136 cx: &mut ViewContext<Self>,137 cx: &mut ViewContext<Self>,
137 ) -> Self {138 ) -> Self {
139 let workspace_handle = workspace.clone();
138 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();140 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
139 cx.subscribe(&terminal, move |this, _, event, cx| match event {141 cx.subscribe(&terminal, move |this, _, event, cx| match event {
140 Event::Wakeup => {142 Event::Wakeup => {
@@ -279,6 +281,7 @@ impl TerminalView { · 138 unmodified lines
279 281
280 Self {282 Self {
281 terminal,283 terminal,
284 workspace: workspace_handle,
282 has_new_content: true,285 has_new_content: true,
283 has_bell: false,286 has_bell: false,
284 focus_handle: cx.focus_handle(),287 focus_handle: cx.focus_handle(),
@@ -661,6 +664,7 @@ impl Render for TerminalView { · 376 unmodified lines
661 // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu664 // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
662 div().size_full().child(TerminalElement::new(665 div().size_full().child(TerminalElement::new(
663 terminal_handle,666 terminal_handle,
667 self.workspace.clone(),
664 self.focus_handle.clone(),668 self.focus_handle.clone(),
665 focused,669 focused,
666 self.should_show_cursor(focused, cx),670 self.should_show_cursor(focused, cx),
crates/theme/src/settings.rs+25−7
@@ -194,9 +194,21 @@ impl settings::Settings for ThemeSettings { · 193 unmodified lines
194 ..Default::default()194 ..Default::default()
195 };195 };
196 196
197 root_schema197 let available_fonts = cx
198 .definitions198 .text_system()
199 .extend([("ThemeName".into(), theme_name_schema.into())]);199 .all_font_families()
200 .into_iter()
201 .map(Value::String)
202 .collect();
203 let fonts_schema = SchemaObject {
204 instance_type: Some(InstanceType::String.into()),
205 enum_values: Some(available_fonts),
206 ..Default::default()
207 };
208 root_schema.definitions.extend([
209 ("ThemeName".into(), theme_name_schema.into()),
210 ("FontFamilies".into(), fonts_schema.into()),
211 ]);
200 212
201 root_schema213 root_schema
202 .schema214 .schema
@@ -204,10 +216,16 @@ impl settings::Settings for ThemeSettings { · 1 unmodified line
204 .as_mut()216 .as_mut()
205 .unwrap()217 .unwrap()
206 .properties218 .properties
207 .extend([(219 .extend([
208 "theme".to_owned(),220 (
209 Schema::new_ref("#/definitions/ThemeName".into()),221 "theme".to_owned(),
210 )]);222 Schema::new_ref("#/definitions/ThemeName".into()),
223 ),
224 (
225 "buffer_font_family".to_owned(),
226 Schema::new_ref("#/definitions/FontFamilies".into()),
227 ),
228 ]);
211 229
212 root_schema230 root_schema
213 }231 }
crates/theme/src/styles/players.rs+1−1
@@ -22,7 +22,7 @@ pub struct PlayerColors(pub Vec<PlayerColor>); · 21 unmodified lines
22impl Default for PlayerColors {22impl Default for PlayerColors {
23 /// Don't use this!23 /// Don't use this!
24 /// We have to have a default to be `[refineable::Refinable]`.24 /// We have to have a default to be `[refineable::Refinable]`.
25 /// todo!("Find a way to not need this for Refinable")25 /// TODO "Find a way to not need this for Refinable"
26 fn default() -> Self {26 fn default() -> Self {
27 Self::dark()27 Self::dark()
28 }28 }
crates/theme/src/themes/andromeda.rs+1−1
@@ -69,7 +69,7 @@ pub fn andromeda() -> UserThemeFamily { · 68 unmodified lines
69 editor_highlighted_line_background: Some(rgba(0x21242bff).into()),69 editor_highlighted_line_background: Some(rgba(0x21242bff).into()),
70 editor_line_number: Some(rgba(0xf7f7f859).into()),70 editor_line_number: Some(rgba(0xf7f7f859).into()),
71 editor_active_line_number: Some(rgba(0xf7f7f8ff).into()),71 editor_active_line_number: Some(rgba(0xf7f7f8ff).into()),
72 editor_invisible: Some(rgba(0xaca8aeff).into()),72 editor_invisible: Some(rgba(0x64646dff).into()),
73 editor_wrap_guide: Some(rgba(0xf7f7f80d).into()),73 editor_wrap_guide: Some(rgba(0xf7f7f80d).into()),
74 editor_active_wrap_guide: Some(rgba(0xf7f7f81a).into()),74 editor_active_wrap_guide: Some(rgba(0xf7f7f81a).into()),
75 editor_document_highlight_read_background: Some(rgba(0x11a7931a).into()),75 editor_document_highlight_read_background: Some(rgba(0x11a7931a).into()),
crates/theme/src/themes/atelier.rs+20−20
@@ -70,7 +70,7 @@ pub fn atelier() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x221f26ff).into()),70 editor_highlighted_line_background: Some(rgba(0x221f26ff).into()),
71 editor_line_number: Some(rgba(0xefecf459).into()),71 editor_line_number: Some(rgba(0xefecf459).into()),
72 editor_active_line_number: Some(rgba(0xefecf4ff).into()),72 editor_active_line_number: Some(rgba(0xefecf4ff).into()),
73 editor_invisible: Some(rgba(0x898591ff).into()),73 editor_invisible: Some(rgba(0x726c7aff).into()),
74 editor_wrap_guide: Some(rgba(0xefecf40d).into()),74 editor_wrap_guide: Some(rgba(0xefecf40d).into()),
75 editor_active_wrap_guide: Some(rgba(0xefecf41a).into()),75 editor_active_wrap_guide: Some(rgba(0xefecf41a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x576dda1a).into()),76 editor_document_highlight_read_background: Some(rgba(0x576dda1a).into()),
@@ -535,7 +535,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
535 editor_highlighted_line_background: Some(rgba(0xe6e3ebff).into()),535 editor_highlighted_line_background: Some(rgba(0xe6e3ebff).into()),
536 editor_line_number: Some(rgba(0x19171c59).into()),536 editor_line_number: Some(rgba(0x19171c59).into()),
537 editor_active_line_number: Some(rgba(0x19171cff).into()),537 editor_active_line_number: Some(rgba(0x19171cff).into()),
538 editor_invisible: Some(rgba(0x5a5462ff).into()),538 editor_invisible: Some(rgba(0x726c7aff).into()),
539 editor_wrap_guide: Some(rgba(0x19171c0d).into()),539 editor_wrap_guide: Some(rgba(0x19171c0d).into()),
540 editor_active_wrap_guide: Some(rgba(0x19171c1a).into()),540 editor_active_wrap_guide: Some(rgba(0x19171c1a).into()),
541 editor_document_highlight_read_background: Some(rgba(0x586dda1a).into()),541 editor_document_highlight_read_background: Some(rgba(0x586dda1a).into()),
@@ -1000,7 +1000,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
1000 editor_highlighted_line_background: Some(rgba(0x262622ff).into()),1000 editor_highlighted_line_background: Some(rgba(0x262622ff).into()),
1001 editor_line_number: Some(rgba(0xfefbec59).into()),1001 editor_line_number: Some(rgba(0xfefbec59).into()),
1002 editor_active_line_number: Some(rgba(0xfefbecff).into()),1002 editor_active_line_number: Some(rgba(0xfefbecff).into()),
1003 editor_invisible: Some(rgba(0xa4a08bff).into()),1003 editor_invisible: Some(rgba(0x8b8874ff).into()),
1004 editor_wrap_guide: Some(rgba(0xfefbec0d).into()),1004 editor_wrap_guide: Some(rgba(0xfefbec0d).into()),
1005 editor_active_wrap_guide: Some(rgba(0xfefbec1a).into()),1005 editor_active_wrap_guide: Some(rgba(0xfefbec1a).into()),
1006 editor_document_highlight_read_background: Some(rgba(0x6684e01a).into()),1006 editor_document_highlight_read_background: Some(rgba(0x6684e01a).into()),
@@ -1465,7 +1465,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
1465 editor_highlighted_line_background: Some(rgba(0xeeebd7ff).into()),1465 editor_highlighted_line_background: Some(rgba(0xeeebd7ff).into()),
1466 editor_line_number: Some(rgba(0x20201d59).into()),1466 editor_line_number: Some(rgba(0x20201d59).into()),
1467 editor_active_line_number: Some(rgba(0x20201dff).into()),1467 editor_active_line_number: Some(rgba(0x20201dff).into()),
1468 editor_invisible: Some(rgba(0x706d5fff).into()),1468 editor_invisible: Some(rgba(0x8b8874ff).into()),
1469 editor_wrap_guide: Some(rgba(0x20201d0d).into()),1469 editor_wrap_guide: Some(rgba(0x20201d0d).into()),
1470 editor_active_wrap_guide: Some(rgba(0x20201d1a).into()),1470 editor_active_wrap_guide: Some(rgba(0x20201d1a).into()),
1471 editor_document_highlight_read_background: Some(rgba(0x6784e01a).into()),1471 editor_document_highlight_read_background: Some(rgba(0x6784e01a).into()),
@@ -1930,7 +1930,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
1930 editor_highlighted_line_background: Some(rgba(0x2c2b23ff).into()),1930 editor_highlighted_line_background: Some(rgba(0x2c2b23ff).into()),
1931 editor_line_number: Some(rgba(0xf4f3ec59).into()),1931 editor_line_number: Some(rgba(0xf4f3ec59).into()),
1932 editor_active_line_number: Some(rgba(0xf4f3ecff).into()),1932 editor_active_line_number: Some(rgba(0xf4f3ecff).into()),
1933 editor_invisible: Some(rgba(0x91907fff).into()),1933 editor_invisible: Some(rgba(0x7a7867ff).into()),
1934 editor_wrap_guide: Some(rgba(0xf4f3ec0d).into()),1934 editor_wrap_guide: Some(rgba(0xf4f3ec0d).into()),
1935 editor_active_wrap_guide: Some(rgba(0xf4f3ec1a).into()),1935 editor_active_wrap_guide: Some(rgba(0xf4f3ec1a).into()),
1936 editor_document_highlight_read_background: Some(rgba(0x37a1661a).into()),1936 editor_document_highlight_read_background: Some(rgba(0x37a1661a).into()),
@@ -2395,7 +2395,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
2395 editor_highlighted_line_background: Some(rgba(0xebeae3ff).into()),2395 editor_highlighted_line_background: Some(rgba(0xebeae3ff).into()),
2396 editor_line_number: Some(rgba(0x22221b59).into()),2396 editor_line_number: Some(rgba(0x22221b59).into()),
2397 editor_active_line_number: Some(rgba(0x22221bff).into()),2397 editor_active_line_number: Some(rgba(0x22221bff).into()),
2398 editor_invisible: Some(rgba(0x61604fff).into()),2398 editor_invisible: Some(rgba(0x7a7867ff).into()),
2399 editor_wrap_guide: Some(rgba(0x22221b0d).into()),2399 editor_wrap_guide: Some(rgba(0x22221b0d).into()),
2400 editor_active_wrap_guide: Some(rgba(0x22221b1a).into()),2400 editor_active_wrap_guide: Some(rgba(0x22221b1a).into()),
2401 editor_document_highlight_read_background: Some(rgba(0x38a1661a).into()),2401 editor_document_highlight_read_background: Some(rgba(0x38a1661a).into()),
@@ -2860,7 +2860,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
2860 editor_highlighted_line_background: Some(rgba(0x27211eff).into()),2860 editor_highlighted_line_background: Some(rgba(0x27211eff).into()),
2861 editor_line_number: Some(rgba(0xf1efee59).into()),2861 editor_line_number: Some(rgba(0xf1efee59).into()),
2862 editor_active_line_number: Some(rgba(0xf1efeeff).into()),2862 editor_active_line_number: Some(rgba(0xf1efeeff).into()),
2863 editor_invisible: Some(rgba(0xa79f9dff).into()),2863 editor_invisible: Some(rgba(0x89817eff).into()),
2864 editor_wrap_guide: Some(rgba(0xf1efee0d).into()),2864 editor_wrap_guide: Some(rgba(0xf1efee0d).into()),
2865 editor_active_wrap_guide: Some(rgba(0xf1efee1a).into()),2865 editor_active_wrap_guide: Some(rgba(0xf1efee1a).into()),
2866 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),2866 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),
@@ -3325,7 +3325,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
3325 editor_highlighted_line_background: Some(rgba(0xe9e6e4ff).into()),3325 editor_highlighted_line_background: Some(rgba(0xe9e6e4ff).into()),
3326 editor_line_number: Some(rgba(0x1b191859).into()),3326 editor_line_number: Some(rgba(0x1b191859).into()),
3327 editor_active_line_number: Some(rgba(0x1b1918ff).into()),3327 editor_active_line_number: Some(rgba(0x1b1918ff).into()),
3328 editor_invisible: Some(rgba(0x6a6360ff).into()),3328 editor_invisible: Some(rgba(0x89817eff).into()),
3329 editor_wrap_guide: Some(rgba(0x1b19180d).into()),3329 editor_wrap_guide: Some(rgba(0x1b19180d).into()),
3330 editor_active_wrap_guide: Some(rgba(0x1b19181a).into()),3330 editor_active_wrap_guide: Some(rgba(0x1b19181a).into()),
3331 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),3331 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),
@@ -3790,7 +3790,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
3790 editor_highlighted_line_background: Some(rgba(0x252025ff).into()),3790 editor_highlighted_line_background: Some(rgba(0x252025ff).into()),
3791 editor_line_number: Some(rgba(0xf7f3f759).into()),3791 editor_line_number: Some(rgba(0xf7f3f759).into()),
3792 editor_active_line_number: Some(rgba(0xf7f3f7ff).into()),3792 editor_active_line_number: Some(rgba(0xf7f3f7ff).into()),
3793 editor_invisible: Some(rgba(0xa99aa9ff).into()),3793 editor_invisible: Some(rgba(0x8b7c8bff).into()),
3794 editor_wrap_guide: Some(rgba(0xf7f3f70d).into()),3794 editor_wrap_guide: Some(rgba(0xf7f3f70d).into()),
3795 editor_active_wrap_guide: Some(rgba(0xf7f3f71a).into()),3795 editor_active_wrap_guide: Some(rgba(0xf7f3f71a).into()),
3796 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),3796 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),
@@ -4255,7 +4255,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
4255 editor_highlighted_line_background: Some(rgba(0xe1d6e1ff).into()),4255 editor_highlighted_line_background: Some(rgba(0xe1d6e1ff).into()),
4256 editor_line_number: Some(rgba(0x1b181b59).into()),4256 editor_line_number: Some(rgba(0x1b181b59).into()),
4257 editor_active_line_number: Some(rgba(0x1b181bff).into()),4257 editor_active_line_number: Some(rgba(0x1b181bff).into()),
4258 editor_invisible: Some(rgba(0x6b5e6bff).into()),4258 editor_invisible: Some(rgba(0x8b7c8bff).into()),
4259 editor_wrap_guide: Some(rgba(0x1b181b0d).into()),4259 editor_wrap_guide: Some(rgba(0x1b181b0d).into()),
4260 editor_active_wrap_guide: Some(rgba(0x1b181b1a).into()),4260 editor_active_wrap_guide: Some(rgba(0x1b181b1a).into()),
4261 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),4261 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),
@@ -4720,7 +4720,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
4720 editor_highlighted_line_background: Some(rgba(0x1c2529ff).into()),4720 editor_highlighted_line_background: Some(rgba(0x1c2529ff).into()),
4721 editor_line_number: Some(rgba(0xebf8ff59).into()),4721 editor_line_number: Some(rgba(0xebf8ff59).into()),
4722 editor_active_line_number: Some(rgba(0xebf8ffff).into()),4722 editor_active_line_number: Some(rgba(0xebf8ffff).into()),
4723 editor_invisible: Some(rgba(0x7ca0b3ff).into()),4723 editor_invisible: Some(rgba(0x66889aff).into()),
4724 editor_wrap_guide: Some(rgba(0xebf8ff0d).into()),4724 editor_wrap_guide: Some(rgba(0xebf8ff0d).into()),
4725 editor_active_wrap_guide: Some(rgba(0xebf8ff1a).into()),4725 editor_active_wrap_guide: Some(rgba(0xebf8ff1a).into()),
4726 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),4726 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),
@@ -5185,7 +5185,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
5185 editor_highlighted_line_background: Some(rgba(0xcdeaf9ff).into()),5185 editor_highlighted_line_background: Some(rgba(0xcdeaf9ff).into()),
5186 editor_line_number: Some(rgba(0x161b1d59).into()),5186 editor_line_number: Some(rgba(0x161b1d59).into()),
5187 editor_active_line_number: Some(rgba(0x161b1dff).into()),5187 editor_active_line_number: Some(rgba(0x161b1dff).into()),
5188 editor_invisible: Some(rgba(0x526f7dff).into()),5188 editor_invisible: Some(rgba(0x66889aff).into()),
5189 editor_wrap_guide: Some(rgba(0x161b1d0d).into()),5189 editor_wrap_guide: Some(rgba(0x161b1d0d).into()),
5190 editor_active_wrap_guide: Some(rgba(0x161b1d1a).into()),5190 editor_active_wrap_guide: Some(rgba(0x161b1d1a).into()),
5191 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),5191 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),
@@ -5650,7 +5650,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
5650 editor_highlighted_line_background: Some(rgba(0x252020ff).into()),5650 editor_highlighted_line_background: Some(rgba(0x252020ff).into()),
5651 editor_line_number: Some(rgba(0xf4ecec59).into()),5651 editor_line_number: Some(rgba(0xf4ecec59).into()),
5652 editor_active_line_number: Some(rgba(0xf4ececff).into()),5652 editor_active_line_number: Some(rgba(0xf4ececff).into()),
5653 editor_invisible: Some(rgba(0x898383ff).into()),5653 editor_invisible: Some(rgba(0x726a6aff).into()),
5654 editor_wrap_guide: Some(rgba(0xf4ecec0d).into()),5654 editor_wrap_guide: Some(rgba(0xf4ecec0d).into()),
5655 editor_active_wrap_guide: Some(rgba(0xf4ecec1a).into()),5655 editor_active_wrap_guide: Some(rgba(0xf4ecec1a).into()),
5656 editor_document_highlight_read_background: Some(rgba(0x7272ca1a).into()),5656 editor_document_highlight_read_background: Some(rgba(0x7272ca1a).into()),
@@ -6115,7 +6115,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
6115 editor_highlighted_line_background: Some(rgba(0xebe3e3ff).into()),6115 editor_highlighted_line_background: Some(rgba(0xebe3e3ff).into()),
6116 editor_line_number: Some(rgba(0x1b181859).into()),6116 editor_line_number: Some(rgba(0x1b181859).into()),
6117 editor_active_line_number: Some(rgba(0x1b1818ff).into()),6117 editor_active_line_number: Some(rgba(0x1b1818ff).into()),
6118 editor_invisible: Some(rgba(0x5a5252ff).into()),6118 editor_invisible: Some(rgba(0x726a6aff).into()),
6119 editor_wrap_guide: Some(rgba(0x1b18180d).into()),6119 editor_wrap_guide: Some(rgba(0x1b18180d).into()),
6120 editor_active_wrap_guide: Some(rgba(0x1b18181a).into()),6120 editor_active_wrap_guide: Some(rgba(0x1b18181a).into()),
6121 editor_document_highlight_read_background: Some(rgba(0x7372ca1a).into()),6121 editor_document_highlight_read_background: Some(rgba(0x7372ca1a).into()),
@@ -6580,7 +6580,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
6580 editor_highlighted_line_background: Some(rgba(0x1f2621ff).into()),6580 editor_highlighted_line_background: Some(rgba(0x1f2621ff).into()),
6581 editor_line_number: Some(rgba(0xecf4ee59).into()),6581 editor_line_number: Some(rgba(0xecf4ee59).into()),
6582 editor_active_line_number: Some(rgba(0xecf4eeff).into()),6582 editor_active_line_number: Some(rgba(0xecf4eeff).into()),
6583 editor_invisible: Some(rgba(0x859188ff).into()),6583 editor_invisible: Some(rgba(0x6c7a71ff).into()),
6584 editor_wrap_guide: Some(rgba(0xecf4ee0d).into()),6584 editor_wrap_guide: Some(rgba(0xecf4ee0d).into()),
6585 editor_active_wrap_guide: Some(rgba(0xecf4ee1a).into()),6585 editor_active_wrap_guide: Some(rgba(0xecf4ee1a).into()),
6586 editor_document_highlight_read_background: Some(rgba(0x478c901a).into()),6586 editor_document_highlight_read_background: Some(rgba(0x478c901a).into()),
@@ -7045,7 +7045,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
7045 editor_highlighted_line_background: Some(rgba(0xe3ebe6ff).into()),7045 editor_highlighted_line_background: Some(rgba(0xe3ebe6ff).into()),
7046 editor_line_number: Some(rgba(0x171c1959).into()),7046 editor_line_number: Some(rgba(0x171c1959).into()),
7047 editor_active_line_number: Some(rgba(0x171c19ff).into()),7047 editor_active_line_number: Some(rgba(0x171c19ff).into()),
7048 editor_invisible: Some(rgba(0x546259ff).into()),7048 editor_invisible: Some(rgba(0x6c7a71ff).into()),
7049 editor_wrap_guide: Some(rgba(0x171c190d).into()),7049 editor_wrap_guide: Some(rgba(0x171c190d).into()),
7050 editor_active_wrap_guide: Some(rgba(0x171c191a).into()),7050 editor_active_wrap_guide: Some(rgba(0x171c191a).into()),
7051 editor_document_highlight_read_background: Some(rgba(0x488c901a).into()),7051 editor_document_highlight_read_background: Some(rgba(0x488c901a).into()),
@@ -7510,7 +7510,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
7510 editor_highlighted_line_background: Some(rgba(0x1f231fff).into()),7510 editor_highlighted_line_background: Some(rgba(0x1f231fff).into()),
7511 editor_line_number: Some(rgba(0xf4fbf459).into()),7511 editor_line_number: Some(rgba(0xf4fbf459).into()),
7512 editor_active_line_number: Some(rgba(0xf4fbf4ff).into()),7512 editor_active_line_number: Some(rgba(0xf4fbf4ff).into()),
7513 editor_invisible: Some(rgba(0x8ba48bff).into()),7513 editor_invisible: Some(rgba(0x748b74ff).into()),
7514 editor_wrap_guide: Some(rgba(0xf4fbf40d).into()),7514 editor_wrap_guide: Some(rgba(0xf4fbf40d).into()),
7515 editor_active_wrap_guide: Some(rgba(0xf4fbf41a).into()),7515 editor_active_wrap_guide: Some(rgba(0xf4fbf41a).into()),
7516 editor_document_highlight_read_background: Some(rgba(0x3e62f41a).into()),7516 editor_document_highlight_read_background: Some(rgba(0x3e62f41a).into()),
@@ -7975,7 +7975,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
7975 editor_highlighted_line_background: Some(rgba(0xdaeedaff).into()),7975 editor_highlighted_line_background: Some(rgba(0xdaeedaff).into()),
7976 editor_line_number: Some(rgba(0x13151359).into()),7976 editor_line_number: Some(rgba(0x13151359).into()),
7977 editor_active_line_number: Some(rgba(0x131513ff).into()),7977 editor_active_line_number: Some(rgba(0x131513ff).into()),
7978 editor_invisible: Some(rgba(0x5f705fff).into()),7978 editor_invisible: Some(rgba(0x748b74ff).into()),
7979 editor_wrap_guide: Some(rgba(0x1315130d).into()),7979 editor_wrap_guide: Some(rgba(0x1315130d).into()),
7980 editor_active_wrap_guide: Some(rgba(0x1315131a).into()),7980 editor_active_wrap_guide: Some(rgba(0x1315131a).into()),
7981 editor_document_highlight_read_background: Some(rgba(0x3f62f41a).into()),7981 editor_document_highlight_read_background: Some(rgba(0x3f62f41a).into()),
@@ -8440,7 +8440,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
8440 editor_highlighted_line_background: Some(rgba(0x262f51ff).into()),8440 editor_highlighted_line_background: Some(rgba(0x262f51ff).into()),
8441 editor_line_number: Some(rgba(0xf5f7ff59).into()),8441 editor_line_number: Some(rgba(0xf5f7ff59).into()),
8442 editor_active_line_number: Some(rgba(0xf5f7ffff).into()),8442 editor_active_line_number: Some(rgba(0xf5f7ffff).into()),
8443 editor_invisible: Some(rgba(0x959bb2ff).into()),8443 editor_invisible: Some(rgba(0x7a819cff).into()),
8444 editor_wrap_guide: Some(rgba(0xf5f7ff0d).into()),8444 editor_wrap_guide: Some(rgba(0xf5f7ff0d).into()),
8445 editor_active_wrap_guide: Some(rgba(0xf5f7ff1a).into()),8445 editor_active_wrap_guide: Some(rgba(0xf5f7ff1a).into()),
8446 editor_document_highlight_read_background: Some(rgba(0x3e8fd01a).into()),8446 editor_document_highlight_read_background: Some(rgba(0x3e8fd01a).into()),
@@ -8905,7 +8905,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
8905 editor_highlighted_line_background: Some(rgba(0xe5e8f5ff).into()),8905 editor_highlighted_line_background: Some(rgba(0xe5e8f5ff).into()),
8906 editor_line_number: Some(rgba(0x20274659).into()),8906 editor_line_number: Some(rgba(0x20274659).into()),
8907 editor_active_line_number: Some(rgba(0x202746ff).into()),8907 editor_active_line_number: Some(rgba(0x202746ff).into()),
8908 editor_invisible: Some(rgba(0x606889ff).into()),8908 editor_invisible: Some(rgba(0x7a819cff).into()),
8909 editor_wrap_guide: Some(rgba(0x2027460d).into()),8909 editor_wrap_guide: Some(rgba(0x2027460d).into()),
8910 editor_active_wrap_guide: Some(rgba(0x2027461a).into()),8910 editor_active_wrap_guide: Some(rgba(0x2027461a).into()),
8911 editor_document_highlight_read_background: Some(rgba(0x3f8fd01a).into()),8911 editor_document_highlight_read_background: Some(rgba(0x3f8fd01a).into()),
crates/theme/src/themes/ayu.rs+3−3
@@ -70,7 +70,7 @@ pub fn ayu() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x1f2127ff).into()),70 editor_highlighted_line_background: Some(rgba(0x1f2127ff).into()),
71 editor_line_number: Some(rgba(0xbfbdb659).into()),71 editor_line_number: Some(rgba(0xbfbdb659).into()),
72 editor_active_line_number: Some(rgba(0xbfbdb6ff).into()),72 editor_active_line_number: Some(rgba(0xbfbdb6ff).into()),
73 editor_invisible: Some(rgba(0x8a8986ff).into()),73 editor_invisible: Some(rgba(0x666767ff).into()),
74 editor_wrap_guide: Some(rgba(0xbfbdb60d).into()),74 editor_wrap_guide: Some(rgba(0xbfbdb60d).into()),
75 editor_active_wrap_guide: Some(rgba(0xbfbdb61a).into()),75 editor_active_wrap_guide: Some(rgba(0xbfbdb61a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x5ac2fe1a).into()),76 editor_document_highlight_read_background: Some(rgba(0x5ac2fe1a).into()),
@@ -514,7 +514,7 @@ pub fn ayu() -> UserThemeFamily { · 437 unmodified lines
514 editor_highlighted_line_background: Some(rgba(0xececedff).into()),514 editor_highlighted_line_background: Some(rgba(0xececedff).into()),
515 editor_line_number: Some(rgba(0x5c616659).into()),515 editor_line_number: Some(rgba(0x5c616659).into()),
516 editor_active_line_number: Some(rgba(0x5c6166ff).into()),516 editor_active_line_number: Some(rgba(0x5c6166ff).into()),
517 editor_invisible: Some(rgba(0x8c8f93ff).into()),517 editor_invisible: Some(rgba(0xacafb1ff).into()),
518 editor_wrap_guide: Some(rgba(0x5c61660d).into()),518 editor_wrap_guide: Some(rgba(0x5c61660d).into()),
519 editor_active_wrap_guide: Some(rgba(0x5c61661a).into()),519 editor_active_wrap_guide: Some(rgba(0x5c61661a).into()),
520 editor_document_highlight_read_background: Some(rgba(0x3b9ee51a).into()),520 editor_document_highlight_read_background: Some(rgba(0x3b9ee51a).into()),
@@ -958,7 +958,7 @@ pub fn ayu() -> UserThemeFamily { · 437 unmodified lines
958 editor_highlighted_line_background: Some(rgba(0x353944ff).into()),958 editor_highlighted_line_background: Some(rgba(0x353944ff).into()),
959 editor_line_number: Some(rgba(0xcccac259).into()),959 editor_line_number: Some(rgba(0xcccac259).into()),
960 editor_active_line_number: Some(rgba(0xcccac2ff).into()),960 editor_active_line_number: Some(rgba(0xcccac2ff).into()),
961 editor_invisible: Some(rgba(0x9a9a98ff).into()),961 editor_invisible: Some(rgba(0x787a7cff).into()),
962 editor_wrap_guide: Some(rgba(0xcccac20d).into()),962 editor_wrap_guide: Some(rgba(0xcccac20d).into()),
963 editor_active_wrap_guide: Some(rgba(0xcccac21a).into()),963 editor_active_wrap_guide: Some(rgba(0xcccac21a).into()),
964 editor_document_highlight_read_background: Some(rgba(0x73cffe1a).into()),964 editor_document_highlight_read_background: Some(rgba(0x73cffe1a).into()),
crates/theme/src/themes/gruvbox.rs+6−6
@@ -70,7 +70,7 @@ pub fn gruvbox() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x3a3735ff).into()),70 editor_highlighted_line_background: Some(rgba(0x3a3735ff).into()),
71 editor_line_number: Some(rgba(0xfbf1c759).into()),71 editor_line_number: Some(rgba(0xfbf1c759).into()),
72 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),72 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
73 editor_invisible: Some(rgba(0xc5b597ff).into()),73 editor_invisible: Some(rgba(0x928474ff).into()),
74 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),74 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
75 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),75 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),76 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -521,7 +521,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
521 editor_highlighted_line_background: Some(rgba(0x393634ff).into()),521 editor_highlighted_line_background: Some(rgba(0x393634ff).into()),
522 editor_line_number: Some(rgba(0xfbf1c759).into()),522 editor_line_number: Some(rgba(0xfbf1c759).into()),
523 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),523 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
524 editor_invisible: Some(rgba(0xc5b597ff).into()),524 editor_invisible: Some(rgba(0x928474ff).into()),
525 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),525 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
526 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),526 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
527 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),527 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -972,7 +972,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
972 editor_highlighted_line_background: Some(rgba(0x3b3735ff).into()),972 editor_highlighted_line_background: Some(rgba(0x3b3735ff).into()),
973 editor_line_number: Some(rgba(0xfbf1c759).into()),973 editor_line_number: Some(rgba(0xfbf1c759).into()),
974 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),974 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
975 editor_invisible: Some(rgba(0xc5b597ff).into()),975 editor_invisible: Some(rgba(0x928474ff).into()),
976 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),976 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
977 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),977 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
978 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),978 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -1423,7 +1423,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
1423 editor_highlighted_line_background: Some(rgba(0xecddb4ff).into()),1423 editor_highlighted_line_background: Some(rgba(0xecddb4ff).into()),
1424 editor_line_number: Some(rgba(0x28282859).into()),1424 editor_line_number: Some(rgba(0x28282859).into()),
1425 editor_active_line_number: Some(rgba(0x282828ff).into()),1425 editor_active_line_number: Some(rgba(0x282828ff).into()),
1426 editor_invisible: Some(rgba(0x5f5650ff).into()),1426 editor_invisible: Some(rgba(0x928474ff).into()),
1427 editor_wrap_guide: Some(rgba(0x2828280d).into()),1427 editor_wrap_guide: Some(rgba(0x2828280d).into()),
1428 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),1428 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
1429 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),1429 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),
@@ -1874,7 +1874,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
1874 editor_highlighted_line_background: Some(rgba(0xecddb5ff).into()),1874 editor_highlighted_line_background: Some(rgba(0xecddb5ff).into()),
1875 editor_line_number: Some(rgba(0x28282859).into()),1875 editor_line_number: Some(rgba(0x28282859).into()),
1876 editor_active_line_number: Some(rgba(0x282828ff).into()),1876 editor_active_line_number: Some(rgba(0x282828ff).into()),
1877 editor_invisible: Some(rgba(0x5f5650ff).into()),1877 editor_invisible: Some(rgba(0x928474ff).into()),
1878 editor_wrap_guide: Some(rgba(0x2828280d).into()),1878 editor_wrap_guide: Some(rgba(0x2828280d).into()),
1879 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),1879 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
1880 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),1880 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),
@@ -2325,7 +2325,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
2325 editor_highlighted_line_background: Some(rgba(0xecdcb3ff).into()),2325 editor_highlighted_line_background: Some(rgba(0xecdcb3ff).into()),
2326 editor_line_number: Some(rgba(0x28282859).into()),2326 editor_line_number: Some(rgba(0x28282859).into()),
2327 editor_active_line_number: Some(rgba(0x282828ff).into()),2327 editor_active_line_number: Some(rgba(0x282828ff).into()),
2328 editor_invisible: Some(rgba(0x5f5650ff).into()),2328 editor_invisible: Some(rgba(0x928474ff).into()),
2329 editor_wrap_guide: Some(rgba(0x2828280d).into()),2329 editor_wrap_guide: Some(rgba(0x2828280d).into()),
2330 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),2330 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
2331 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),2331 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),
crates/theme/src/themes/one.rs+2−2
@@ -70,7 +70,7 @@ pub fn one() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x2f343eff).into()),70 editor_highlighted_line_background: Some(rgba(0x2f343eff).into()),
71 editor_line_number: Some(rgba(0xc8ccd459).into()),71 editor_line_number: Some(rgba(0xc8ccd459).into()),
72 editor_active_line_number: Some(rgba(0xc8ccd4ff).into()),72 editor_active_line_number: Some(rgba(0xc8ccd4ff).into()),
73 editor_invisible: Some(rgba(0x838994ff).into()),73 editor_invisible: Some(rgba(0x555a63ff).into()),
74 editor_wrap_guide: Some(rgba(0xc8ccd40d).into()),74 editor_wrap_guide: Some(rgba(0xc8ccd40d).into()),
75 editor_active_wrap_guide: Some(rgba(0xc8ccd41a).into()),75 editor_active_wrap_guide: Some(rgba(0xc8ccd41a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x74ade81a).into()),76 editor_document_highlight_read_background: Some(rgba(0x74ade81a).into()),
@@ -521,7 +521,7 @@ pub fn one() -> UserThemeFamily { · 444 unmodified lines
521 editor_highlighted_line_background: Some(rgba(0xebebecff).into()),521 editor_highlighted_line_background: Some(rgba(0xebebecff).into()),
522 editor_line_number: Some(rgba(0x383a4159).into()),522 editor_line_number: Some(rgba(0x383a4159).into()),
523 editor_active_line_number: Some(rgba(0x383a41ff).into()),523 editor_active_line_number: Some(rgba(0x383a41ff).into()),
524 editor_invisible: Some(rgba(0x7f8188ff).into()),524 editor_invisible: Some(rgba(0xa3a3a4ff).into()),
525 editor_wrap_guide: Some(rgba(0x383a410d).into()),525 editor_wrap_guide: Some(rgba(0x383a410d).into()),
526 editor_active_wrap_guide: Some(rgba(0x383a411a).into()),526 editor_active_wrap_guide: Some(rgba(0x383a411a).into()),
527 editor_document_highlight_read_background: Some(rgba(0x5c79e21a).into()),527 editor_document_highlight_read_background: Some(rgba(0x5c79e21a).into()),
crates/theme/src/themes/rose_pine.rs+3−3
@@ -70,7 +70,7 @@ pub fn rose_pine() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x1d1b2aff).into()),70 editor_highlighted_line_background: Some(rgba(0x1d1b2aff).into()),
71 editor_line_number: Some(rgba(0xe0def459).into()),71 editor_line_number: Some(rgba(0xe0def459).into()),
72 editor_active_line_number: Some(rgba(0xe0def4ff).into()),72 editor_active_line_number: Some(rgba(0xe0def4ff).into()),
73 editor_invisible: Some(rgba(0x75718eff).into()),73 editor_invisible: Some(rgba(0x28253cff).into()),
74 editor_wrap_guide: Some(rgba(0xe0def40d).into()),74 editor_wrap_guide: Some(rgba(0xe0def40d).into()),
75 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),75 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()),76 editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()),
@@ -528,7 +528,7 @@ pub fn rose_pine() -> UserThemeFamily { · 451 unmodified lines
528 editor_highlighted_line_background: Some(rgba(0xfef9f2ff).into()),528 editor_highlighted_line_background: Some(rgba(0xfef9f2ff).into()),
529 editor_line_number: Some(rgba(0x57527959).into()),529 editor_line_number: Some(rgba(0x57527959).into()),
530 editor_active_line_number: Some(rgba(0x575279ff).into()),530 editor_active_line_number: Some(rgba(0x575279ff).into()),
531 editor_invisible: Some(rgba(0x706c8cff).into()),531 editor_invisible: Some(rgba(0x9691a4ff).into()),
532 editor_wrap_guide: Some(rgba(0x5752790d).into()),532 editor_wrap_guide: Some(rgba(0x5752790d).into()),
533 editor_active_wrap_guide: Some(rgba(0x5752791a).into()),533 editor_active_wrap_guide: Some(rgba(0x5752791a).into()),
534 editor_document_highlight_read_background: Some(rgba(0x57949f1a).into()),534 editor_document_highlight_read_background: Some(rgba(0x57949f1a).into()),
@@ -986,7 +986,7 @@ pub fn rose_pine() -> UserThemeFamily { · 451 unmodified lines
986 editor_highlighted_line_background: Some(rgba(0x28253cff).into()),986 editor_highlighted_line_background: Some(rgba(0x28253cff).into()),
987 editor_line_number: Some(rgba(0xe0def459).into()),987 editor_line_number: Some(rgba(0xe0def459).into()),
988 editor_active_line_number: Some(rgba(0xe0def4ff).into()),988 editor_active_line_number: Some(rgba(0xe0def4ff).into()),
989 editor_invisible: Some(rgba(0x85819eff).into()),989 editor_invisible: Some(rgba(0x595571ff).into()),
990 editor_wrap_guide: Some(rgba(0xe0def40d).into()),990 editor_wrap_guide: Some(rgba(0xe0def40d).into()),
991 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),991 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),
992 editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()),992 editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()),
crates/theme/src/themes/sandcastle.rs+1−1
@@ -69,7 +69,7 @@ pub fn sandcastle() -> UserThemeFamily { · 68 unmodified lines
69 editor_highlighted_line_background: Some(rgba(0x2b3039ff).into()),69 editor_highlighted_line_background: Some(rgba(0x2b3039ff).into()),
70 editor_line_number: Some(rgba(0xfdf4c159).into()),70 editor_line_number: Some(rgba(0xfdf4c159).into()),
71 editor_active_line_number: Some(rgba(0xfdf4c1ff).into()),71 editor_active_line_number: Some(rgba(0xfdf4c1ff).into()),
72 editor_invisible: Some(rgba(0xa69782ff).into()),72 editor_invisible: Some(rgba(0x7c6f64ff).into()),
73 editor_wrap_guide: Some(rgba(0xfdf4c10d).into()),73 editor_wrap_guide: Some(rgba(0xfdf4c10d).into()),
74 editor_active_wrap_guide: Some(rgba(0xfdf4c11a).into()),74 editor_active_wrap_guide: Some(rgba(0xfdf4c11a).into()),
75 editor_document_highlight_read_background: Some(rgba(0x528b8b1a).into()),75 editor_document_highlight_read_background: Some(rgba(0x528b8b1a).into()),
crates/theme/src/themes/solarized.rs+2−2
@@ -70,7 +70,7 @@ pub fn solarized() -> UserThemeFamily { · 69 unmodified lines
70 editor_highlighted_line_background: Some(rgba(0x04313cff).into()),70 editor_highlighted_line_background: Some(rgba(0x04313cff).into()),
71 editor_line_number: Some(rgba(0xfdf6e359).into()),71 editor_line_number: Some(rgba(0xfdf6e359).into()),
72 editor_active_line_number: Some(rgba(0xfdf6e3ff).into()),72 editor_active_line_number: Some(rgba(0xfdf6e3ff).into()),
73 editor_invisible: Some(rgba(0x93a1a1ff).into()),73 editor_invisible: Some(rgba(0x6d8288ff).into()),
74 editor_wrap_guide: Some(rgba(0xfdf6e30d).into()),74 editor_wrap_guide: Some(rgba(0xfdf6e30d).into()),
75 editor_active_wrap_guide: Some(rgba(0xfdf6e31a).into()),75 editor_active_wrap_guide: Some(rgba(0xfdf6e31a).into()),
76 editor_document_highlight_read_background: Some(rgba(0x288bd11a).into()),76 editor_document_highlight_read_background: Some(rgba(0x288bd11a).into()),
@@ -514,7 +514,7 @@ pub fn solarized() -> UserThemeFamily { · 437 unmodified lines
514 editor_highlighted_line_background: Some(rgba(0xf3eddaff).into()),514 editor_highlighted_line_background: Some(rgba(0xf3eddaff).into()),
515 editor_line_number: Some(rgba(0x002b3659).into()),515 editor_line_number: Some(rgba(0x002b3659).into()),
516 editor_active_line_number: Some(rgba(0x002b36ff).into()),516 editor_active_line_number: Some(rgba(0x002b36ff).into()),
517 editor_invisible: Some(rgba(0x34555eff).into()),517 editor_invisible: Some(rgba(0x6d8288ff).into()),
518 editor_wrap_guide: Some(rgba(0x002b360d).into()),518 editor_wrap_guide: Some(rgba(0x002b360d).into()),
519 editor_active_wrap_guide: Some(rgba(0x002b361a).into()),519 editor_active_wrap_guide: Some(rgba(0x002b361a).into()),
520 editor_document_highlight_read_background: Some(rgba(0x298bd11a).into()),520 editor_document_highlight_read_background: Some(rgba(0x298bd11a).into()),
crates/theme/src/themes/summercamp.rs+1−1
@@ -69,7 +69,7 @@ pub fn summercamp() -> UserThemeFamily { · 68 unmodified lines
69 editor_highlighted_line_background: Some(rgba(0x231f16ff).into()),69 editor_highlighted_line_background: Some(rgba(0x231f16ff).into()),
70 editor_line_number: Some(rgba(0xf8f5de59).into()),70 editor_line_number: Some(rgba(0xf8f5de59).into()),
71 editor_active_line_number: Some(rgba(0xf8f5deff).into()),71 editor_active_line_number: Some(rgba(0xf8f5deff).into()),
72 editor_invisible: Some(rgba(0x736e55ff).into()),72 editor_invisible: Some(rgba(0x494433ff).into()),
73 editor_wrap_guide: Some(rgba(0xf8f5de0d).into()),73 editor_wrap_guide: Some(rgba(0xf8f5de0d).into()),
74 editor_active_wrap_guide: Some(rgba(0xf8f5de1a).into()),74 editor_active_wrap_guide: Some(rgba(0xf8f5de1a).into()),
75 editor_document_highlight_read_background: Some(rgba(0x499bef1a).into()),75 editor_document_highlight_read_background: Some(rgba(0x499bef1a).into()),
crates/theme_importer/src/zed1/converter.rs+1−1
@@ -240,7 +240,7 @@ impl Zed1ThemeConverter { · 239 unmodified lines
240 editor_highlighted_line_background: convert(editor.highlighted_line_background),240 editor_highlighted_line_background: convert(editor.highlighted_line_background),
241 editor_line_number: convert(editor.line_number),241 editor_line_number: convert(editor.line_number),
242 editor_active_line_number: convert(editor.line_number_active),242 editor_active_line_number: convert(editor.line_number_active),
243 editor_invisible: convert(highest.variant.default.foreground), // TODO: Is this light enough?243 editor_invisible: convert(editor.whitespace),
244 editor_wrap_guide: convert(editor.wrap_guide),244 editor_wrap_guide: convert(editor.wrap_guide),
245 editor_active_wrap_guide: convert(editor.active_wrap_guide),245 editor_active_wrap_guide: convert(editor.active_wrap_guide),
246 editor_document_highlight_read_background: convert(246 editor_document_highlight_read_background: convert(
crates/util/src/util.rs+11−0
@@ -137,6 +137,8 @@ pub trait ResultExt<E> { · 136 unmodified lines
137 type Ok;137 type Ok;
138 138
139 fn log_err(self) -> Option<Self::Ok>;139 fn log_err(self) -> Option<Self::Ok>;
140 /// Assert that this result should never be an error in development or tests.
141 fn debug_assert_ok(self, reason: &str) -> Self;
140 fn warn_on_err(self) -> Option<Self::Ok>;142 fn warn_on_err(self) -> Option<Self::Ok>;
141 fn inspect_error(self, func: impl FnOnce(&E)) -> Self;143 fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
142}144}
@@ -159,6 +161,14 @@ where · 16 unmodified lines
159 }161 }
160 }162 }
161 163
164 #[track_caller]
165 fn debug_assert_ok(self, reason: &str) -> Self {
166 if let Err(error) = &self {
167 debug_panic!("{reason} - {error:?}");
168 }
169 self
170 }
171
162 fn warn_on_err(self) -> Option<T> {172 fn warn_on_err(self) -> Option<T> {
163 match self {173 match self {
164 Ok(value) => Some(value),174 Ok(value) => Some(value),
@@ -234,6 +244,7 @@ where · 69 unmodified lines
234 }244 }
235}245}
236 246
247#[must_use]
237pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);248pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
238 249
239impl<F, T, E> Future for LogErrorFuture<F>250impl<F, T, E> Future for LogErrorFuture<F>
crates/vim/src/editor_events.rs+0−1
@@ -111,7 +111,6 @@ mod test { · 110 unmodified lines
111 111
112 let mut cx1 = VisualTestContext::from_window(cx.window, &cx);112 let mut cx1 = VisualTestContext::from_window(cx.window, &cx);
113 let editor1 = cx.editor.clone();113 let editor1 = cx.editor.clone();
114 dbg!(editor1.entity_id());
115 114
116 let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));115 let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
117 let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx));116 let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx));
crates/workspace/src/pane.rs+40−16
@@ -6,6 +6,7 @@ use crate::{ · 5 unmodified lines
6};6};
7use anyhow::Result;7use anyhow::Result;
8use collections::{HashMap, HashSet, VecDeque};8use collections::{HashMap, HashSet, VecDeque};
9use futures::{stream::FuturesUnordered, StreamExt};
9use gpui::{10use gpui::{
10 actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,11 actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
11 AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,12 AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,
@@ -255,8 +256,8 @@ impl Pane { · 243 unmodified lines
255 let focus_handle = cx.focus_handle();256 let focus_handle = cx.focus_handle();
256 257
257 let subscriptions = vec![258 let subscriptions = vec![
258 cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)),259 cx.on_focus_in(&focus_handle, Pane::focus_in),
259 cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)),260 cx.on_focus_out(&focus_handle, Pane::focus_out),
260 ];261 ];
261 262
262 let handle = cx.view().downgrade();263 let handle = cx.view().downgrade();
@@ -1796,23 +1797,46 @@ impl Pane { · 1533 unmodified lines
1796 }1797 }
1797 }1798 }
1798 let mut to_pane = cx.view().clone();1799 let mut to_pane = cx.view().clone();
1799 let split_direction = self.drag_split_direction;1800 let mut split_direction = self.drag_split_direction;
1800 let paths = paths.paths().to_vec();1801 let paths = paths.paths().to_vec();
1801 self.workspace1802 self.workspace
1802 .update(cx, |_, cx| {1803 .update(cx, |workspace, cx| {
1803 cx.defer(move |workspace, cx| {1804 let fs = Arc::clone(workspace.project().read(cx).fs());
1804 if let Some(split_direction) = split_direction {1805 cx.spawn(|workspace, mut cx| async move {
1805 to_pane = workspace.split_pane(to_pane, split_direction, cx);1806 let mut is_file_checks = FuturesUnordered::new();
1807 for path in &paths {
1808 is_file_checks.push(fs.is_file(path))
1806 }1809 }
1807 workspace1810 let mut has_files_to_open = false;
1808 .open_paths(1811 while let Some(is_file) = is_file_checks.next().await {
1809 paths,1812 if is_file {
1810 OpenVisible::OnlyDirectories,1813 has_files_to_open = true;
1811 Some(to_pane.downgrade()),1814 break;
1812 cx,1815 }
1813 )1816 }
1814 .detach();1817 drop(is_file_checks);
1815 });1818 if !has_files_to_open {
1819 split_direction = None;
1820 }
1821
1822 if let Some(open_task) = workspace
1823 .update(&mut cx, |workspace, cx| {
1824 if let Some(split_direction) = split_direction {
1825 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1826 }
1827 workspace.open_paths(
1828 paths,
1829 OpenVisible::OnlyDirectories,
1830 Some(to_pane.downgrade()),
1831 cx,
1832 )
1833 })
1834 .ok()
1835 {
1836 let _opened_items: Vec<_> = open_task.await;
1837 }
1838 })
1839 .detach();
1816 })1840 })
1817 .log_err();1841 .log_err();
1818 }1842 }
crates/workspace/src/workspace.rs+427−363
@@ -512,6 +512,11 @@ impl Workspace { · 511 unmodified lines
512 512
513 project::Event::DisconnectedFromHost => {513 project::Event::DisconnectedFromHost => {
514 this.update_window_edited(cx);514 this.update_window_edited(cx);
515 let panes_to_unfollow: Vec<View<Pane>> =
516 this.follower_states.keys().map(|k| k.clone()).collect();
517 for pane in panes_to_unfollow {
518 this.unfollow(&pane, cx);
519 }
515 cx.disable_focus();520 cx.disable_focus();
516 }521 }
517 522
@@ -672,7 +677,7 @@ impl Workspace { · 154 unmodified lines
672 // );677 // );
673 678
674 // this.show_notification(1, cx, |cx| {679 // this.show_notification(1, cx, |cx| {
675 // cx.build_view(|_cx| {680 // cx.new_view(|_cx| {
676 // simple_message_notification::MessageNotification::new(format!("Error:"))681 // simple_message_notification::MessageNotification::new(format!("Error:"))
677 // .with_click_message("click here because!")682 // .with_click_message("click here because!")
678 // })683 // })
@@ -2603,11 +2608,20 @@ impl Workspace { · 1924 unmodified lines
2603 let cx = &cx;2608 let cx = &cx;
2604 move |item| {2609 move |item| {
2605 let item = item.to_followable_item_handle(cx)?;2610 let item = item.to_followable_item_handle(cx)?;
2606 if (project_id.is_none() || project_id != follower_project_id)2611
2607 && item.is_project_item(cx)2612 // If the item belongs to a particular project, then it should
2613 // only be included if this project is shared, and the follower
2614 // is in thie project.
2615 //
2616 // Some items, like channel notes, do not belong to a particular
2617 // project, so they should be included regardless of whether the
2618 // current project is shared, or what project the follower is in.
2619 if item.is_project_item(cx)
2620 && (project_id.is_none() || project_id != follower_project_id)
2608 {2621 {
2609 return None;2622 return None;
2610 }2623 }
2624
2611 let id = item.remote_id(client, cx)?.to_proto();2625 let id = item.remote_id(client, cx)?.to_proto();
2612 let variant = item.to_state_proto(cx)?;2626 let variant = item.to_state_proto(cx)?;
2613 Some(proto::View {2627 Some(proto::View {
@@ -2785,8 +2799,12 @@ impl Workspace { · 171 unmodified lines
2785 update: proto::update_followers::Variant,2799 update: proto::update_followers::Variant,
2786 cx: &mut WindowContext,2800 cx: &mut WindowContext,
2787 ) -> Option<()> {2801 ) -> Option<()> {
2802 // If this update only applies to for followers in the current project,
2803 // then skip it unless this project is shared. If it applies to all
2804 // followers, regardless of project, then set `project_id` to none,
2805 // indicating that it goes to all followers.
2788 let project_id = if project_only {2806 let project_id = if project_only {
2789 self.project.read(cx).remote_id()2807 Some(self.project.read(cx).remote_id()?)
2790 } else {2808 } else {
2791 None2809 None
2792 };2810 };
@@ -4363,12 +4381,15 @@ mod tests { · 1570 unmodified lines
4363 use std::{cell::RefCell, rc::Rc};4381 use std::{cell::RefCell, rc::Rc};
4364 4382
4365 use super::*;4383 use super::*;
4366 use crate::item::{4384 use crate::{
4367 test::{TestItem, TestProjectItem},4385 dock::{test::TestPanel, PanelEvent},
4368 ItemEvent,4386 item::{
4387 test::{TestItem, TestProjectItem},
4388 ItemEvent,
4389 },
4369 };4390 };
4370 use fs::FakeFs;4391 use fs::FakeFs;
4371 use gpui::TestAppContext;4392 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4372 use project::{Project, ProjectEntryId};4393 use project::{Project, ProjectEntryId};
4373 use serde_json::json;4394 use serde_json::json;
4374 use settings::SettingsStore;4395 use settings::SettingsStore;
@@ -4935,362 +4956,405 @@ mod tests { · 560 unmodified lines
4935 });4956 });
4936 }4957 }
4937 4958
4938 // #[gpui::test]4959 #[gpui::test]
4939 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {4960 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4940 // init_test(cx);4961 init_test(cx);
4941 // let fs = FakeFs::new(cx.executor());4962 let fs = FakeFs::new(cx.executor());
4942
4943 // let project = Project::test(fs, [], cx).await;
4944 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4945
4946 // let panel = workspace.update(cx, |workspace, cx| {
4947 // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
4948 // workspace.add_panel(panel.clone(), cx);
4949
4950 // workspace
4951 // .right_dock()
4952 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4953
4954 // panel
4955 // });
4956
4957 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4958 // pane.update(cx, |pane, cx| {
4959 // let item = cx.build_view(|cx| TestItem::new(cx));
4960 // pane.add_item(Box::new(item), true, true, None, cx);
4961 // });
4962
4963 // // Transfer focus from center to panel
4964 // workspace.update(cx, |workspace, cx| {
4965 // workspace.toggle_panel_focus::<TestPanel>(cx);
4966 // });
4967
4968 // workspace.update(cx, |workspace, cx| {
4969 // assert!(workspace.right_dock().read(cx).is_open());
4970 // assert!(!panel.is_zoomed(cx));
4971 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4972 // });
4973
4974 // // Transfer focus from panel to center
4975 // workspace.update(cx, |workspace, cx| {
4976 // workspace.toggle_panel_focus::<TestPanel>(cx);
4977 // });
4978
4979 // workspace.update(cx, |workspace, cx| {
4980 // assert!(workspace.right_dock().read(cx).is_open());
4981 // assert!(!panel.is_zoomed(cx));
4982 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4983 // });
4984
4985 // // Close the dock
4986 // workspace.update(cx, |workspace, cx| {
4987 // workspace.toggle_dock(DockPosition::Right, cx);
4988 // });
4989
4990 // workspace.update(cx, |workspace, cx| {
4991 // assert!(!workspace.right_dock().read(cx).is_open());
4992 // assert!(!panel.is_zoomed(cx));
4993 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4994 // });
4995
4996 // // Open the dock
4997 // workspace.update(cx, |workspace, cx| {
4998 // workspace.toggle_dock(DockPosition::Right, cx);
4999 // });
5000
5001 // workspace.update(cx, |workspace, cx| {
5002 // assert!(workspace.right_dock().read(cx).is_open());
5003 // assert!(!panel.is_zoomed(cx));
5004 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5005 // });
5006
5007 // // Focus and zoom panel
5008 // panel.update(cx, |panel, cx| {
5009 // cx.focus_self();
5010 // panel.set_zoomed(true, cx)
5011 // });
5012
5013 // workspace.update(cx, |workspace, cx| {
5014 // assert!(workspace.right_dock().read(cx).is_open());
5015 // assert!(panel.is_zoomed(cx));
5016 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5017 // });
5018
5019 // // Transfer focus to the center closes the dock
5020 // workspace.update(cx, |workspace, cx| {
5021 // workspace.toggle_panel_focus::<TestPanel>(cx);
5022 // });
5023
5024 // workspace.update(cx, |workspace, cx| {
5025 // assert!(!workspace.right_dock().read(cx).is_open());
5026 // assert!(panel.is_zoomed(cx));
5027 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5028 // });
5029
5030 // // Transferring focus back to the panel keeps it zoomed
5031 // workspace.update(cx, |workspace, cx| {
5032 // workspace.toggle_panel_focus::<TestPanel>(cx);
5033 // });
5034
5035 // workspace.update(cx, |workspace, cx| {
5036 // assert!(workspace.right_dock().read(cx).is_open());
5037 // assert!(panel.is_zoomed(cx));
5038 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5039 // });
5040
5041 // // Close the dock while it is zoomed
5042 // workspace.update(cx, |workspace, cx| {
5043 // workspace.toggle_dock(DockPosition::Right, cx)
5044 // });
5045
5046 // workspace.update(cx, |workspace, cx| {
5047 // assert!(!workspace.right_dock().read(cx).is_open());
5048 // assert!(panel.is_zoomed(cx));
5049 // assert!(workspace.zoomed.is_none());
5050 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5051 // });
5052
5053 // // Opening the dock, when it's zoomed, retains focus
5054 // workspace.update(cx, |workspace, cx| {
5055 // workspace.toggle_dock(DockPosition::Right, cx)
5056 // });
5057
5058 // workspace.update(cx, |workspace, cx| {
5059 // assert!(workspace.right_dock().read(cx).is_open());
5060 // assert!(panel.is_zoomed(cx));
5061 // assert!(workspace.zoomed.is_some());
5062 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5063 // });
5064
5065 // // Unzoom and close the panel, zoom the active pane.
5066 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5067 // workspace.update(cx, |workspace, cx| {
5068 // workspace.toggle_dock(DockPosition::Right, cx)
5069 // });
5070 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5071
5072 // // Opening a dock unzooms the pane.
5073 // workspace.update(cx, |workspace, cx| {
5074 // workspace.toggle_dock(DockPosition::Right, cx)
5075 // });
5076 // workspace.update(cx, |workspace, cx| {
5077 // let pane = pane.read(cx);
5078 // assert!(!pane.is_zoomed());
5079 // assert!(!pane.focus_handle(cx).is_focused(cx));
5080 // assert!(workspace.right_dock().read(cx).is_open());
5081 // assert!(workspace.zoomed.is_none());
5082 // });
5083 // }
5084 4963
5085 // #[gpui::test]4964 let project = Project::test(fs, [], cx).await;
5086 // async fn test_panels(cx: &mut gpui::TestAppContext) {4965 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5087 // init_test(cx);4966
5088 // let fs = FakeFs::new(cx.executor());4967 let panel = workspace.update(cx, |workspace, cx| {
5089 4968 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5090 // let project = Project::test(fs, [], cx).await;4969 workspace.add_panel(panel.clone(), cx);
5091 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));4970
5092 4971 workspace
5093 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {4972 .right_dock()
5094 // // Add panel_1 on the left, panel_2 on the right.4973 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5095 // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));4974
5096 // workspace.add_panel(panel_1.clone(), cx);4975 panel
5097 // workspace4976 });
5098 // .left_dock()4977
5099 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));4978 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5100 // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));4979 pane.update(cx, |pane, cx| {
5101 // workspace.add_panel(panel_2.clone(), cx);4980 let item = cx.new_view(|cx| TestItem::new(cx));
5102 // workspace4981 pane.add_item(Box::new(item), true, true, None, cx);
5103 // .right_dock()4982 });
5104 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));4983
5105 4984 // Transfer focus from center to panel
5106 // let left_dock = workspace.left_dock();4985 workspace.update(cx, |workspace, cx| {
5107 // assert_eq!(4986 workspace.toggle_panel_focus::<TestPanel>(cx);
5108 // left_dock.read(cx).visible_panel().unwrap().panel_id(),4987 });
5109 // panel_1.panel_id()4988
5110 // );4989 workspace.update(cx, |workspace, cx| {
5111 // assert_eq!(4990 assert!(workspace.right_dock().read(cx).is_open());
5112 // left_dock.read(cx).active_panel_size(cx).unwrap(),4991 assert!(!panel.is_zoomed(cx));
5113 // panel_1.size(cx)4992 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5114 // );4993 });
5115 4994
5116 // left_dock.update(cx, |left_dock, cx| {4995 // Transfer focus from panel to center
5117 // left_dock.resize_active_panel(Some(1337.), cx)4996 workspace.update(cx, |workspace, cx| {
5118 // });4997 workspace.toggle_panel_focus::<TestPanel>(cx);
5119 // assert_eq!(4998 });
5120 // workspace4999
5121 // .right_dock()5000 workspace.update(cx, |workspace, cx| {
5122 // .read(cx)5001 assert!(workspace.right_dock().read(cx).is_open());
5123 // .visible_panel()5002 assert!(!panel.is_zoomed(cx));
5124 // .unwrap()5003 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5125 // .panel_id(),5004 });
5126 // panel_2.panel_id(),5005
5127 // );5006 // Close the dock
5128 5007 workspace.update(cx, |workspace, cx| {
5129 // (panel_1, panel_2)5008 workspace.toggle_dock(DockPosition::Right, cx);
5130 // });5009 });
5131 5010
5132 // // Move panel_1 to the right5011 workspace.update(cx, |workspace, cx| {
5133 // panel_1.update(cx, |panel_1, cx| {5012 assert!(!workspace.right_dock().read(cx).is_open());
5134 // panel_1.set_position(DockPosition::Right, cx)5013 assert!(!panel.is_zoomed(cx));
5135 // });5014 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5136 5015 });
5137 // workspace.update(cx, |workspace, cx| {5016
5138 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.5017 // Open the dock
5139 // // Since it was the only panel on the left, the left dock should now be closed.5018 workspace.update(cx, |workspace, cx| {
5140 // assert!(!workspace.left_dock().read(cx).is_open());5019 workspace.toggle_dock(DockPosition::Right, cx);
5141 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());5020 });
5142 // let right_dock = workspace.right_dock();5021
5143 // assert_eq!(5022 workspace.update(cx, |workspace, cx| {
5144 // right_dock.read(cx).visible_panel().unwrap().panel_id(),5023 assert!(workspace.right_dock().read(cx).is_open());
5145 // panel_1.panel_id()5024 assert!(!panel.is_zoomed(cx));
5146 // );5025 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5147 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);5026 });
5148 5027
5149 // // Now we move panel_2 to the left5028 // Focus and zoom panel
5150 // panel_2.set_position(DockPosition::Left, cx);5029 panel.update(cx, |panel, cx| {
5151 // });5030 cx.focus_self();
5152 5031 panel.set_zoomed(true, cx)
5153 // workspace.update(cx, |workspace, cx| {5032 });
5154 // // Since panel_2 was not visible on the right, we don't open the left dock.5033
5155 // assert!(!workspace.left_dock().read(cx).is_open());5034 workspace.update(cx, |workspace, cx| {
5156 // // And the right dock is unaffected in it's displaying of panel_15035 assert!(workspace.right_dock().read(cx).is_open());
5157 // assert!(workspace.right_dock().read(cx).is_open());5036 assert!(panel.is_zoomed(cx));
5158 // assert_eq!(5037 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5159 // workspace5038 });
5160 // .right_dock()5039
5161 // .read(cx)5040 // Transfer focus to the center closes the dock
5162 // .visible_panel()5041 workspace.update(cx, |workspace, cx| {
5163 // .unwrap()5042 workspace.toggle_panel_focus::<TestPanel>(cx);
5164 // .panel_id(),5043 });
5165 // panel_1.panel_id(),5044
5166 // );5045 workspace.update(cx, |workspace, cx| {
5167 // });5046 assert!(!workspace.right_dock().read(cx).is_open());
5168 5047 assert!(panel.is_zoomed(cx));
5169 // // Move panel_1 back to the left5048 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5170 // panel_1.update(cx, |panel_1, cx| {5049 });
5171 // panel_1.set_position(DockPosition::Left, cx)5050
5172 // });5051 // Transferring focus back to the panel keeps it zoomed
5173 5052 workspace.update(cx, |workspace, cx| {
5174 // workspace.update(cx, |workspace, cx| {5053 workspace.toggle_panel_focus::<TestPanel>(cx);
5175 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.5054 });
5176 // let left_dock = workspace.left_dock();5055
5177 // assert!(left_dock.read(cx).is_open());5056 workspace.update(cx, |workspace, cx| {
5178 // assert_eq!(5057 assert!(workspace.right_dock().read(cx).is_open());
5179 // left_dock.read(cx).visible_panel().unwrap().panel_id(),5058 assert!(panel.is_zoomed(cx));
5180 // panel_1.panel_id()5059 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5181 // );5060 });
5182 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);5061
5183 // // And right the dock should be closed as it no longer has any panels.5062 // Close the dock while it is zoomed
5184 // assert!(!workspace.right_dock().read(cx).is_open());5063 workspace.update(cx, |workspace, cx| {
5185 5064 workspace.toggle_dock(DockPosition::Right, cx)
5186 // // Now we move panel_1 to the bottom5065 });
5187 // panel_1.set_position(DockPosition::Bottom, cx);5066
5188 // });5067 workspace.update(cx, |workspace, cx| {
5189 5068 assert!(!workspace.right_dock().read(cx).is_open());
5190 // workspace.update(cx, |workspace, cx| {5069 assert!(panel.is_zoomed(cx));
5191 // // Since panel_1 was visible on the left, we close the left dock.5070 assert!(workspace.zoomed.is_none());
5192 // assert!(!workspace.left_dock().read(cx).is_open());5071 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5193 // // The bottom dock is sized based on the panel's default size,5072 });
5194 // // since the panel orientation changed from vertical to horizontal.5073
5195 // let bottom_dock = workspace.bottom_dock();5074 // Opening the dock, when it's zoomed, retains focus
5196 // assert_eq!(5075 workspace.update(cx, |workspace, cx| {
5197 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),5076 workspace.toggle_dock(DockPosition::Right, cx)
5198 // panel_1.size(cx),5077 });
5199 // );5078
5200 // // Close bottom dock and move panel_1 back to the left.5079 workspace.update(cx, |workspace, cx| {
5201 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));5080 assert!(workspace.right_dock().read(cx).is_open());
5202 // panel_1.set_position(DockPosition::Left, cx);5081 assert!(panel.is_zoomed(cx));
5203 // });5082 assert!(workspace.zoomed.is_some());
5204 5083 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5205 // // Emit activated event on panel 15084 });
5206 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));5085
5207 5086 // Unzoom and close the panel, zoom the active pane.
5208 // // Now the left dock is open and panel_1 is active and focused.5087 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5209 // workspace.update(cx, |workspace, cx| {5088 workspace.update(cx, |workspace, cx| {
5210 // let left_dock = workspace.left_dock();5089 workspace.toggle_dock(DockPosition::Right, cx)
5211 // assert!(left_dock.read(cx).is_open());5090 });
5212 // assert_eq!(5091 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5213 // left_dock.read(cx).visible_panel().unwrap().panel_id(),5092
5214 // panel_1.panel_id(),5093 // Opening a dock unzooms the pane.
5215 // );5094 workspace.update(cx, |workspace, cx| {
5216 // assert!(panel_1.focus_handle(cx).is_focused(cx));5095 workspace.toggle_dock(DockPosition::Right, cx)
5217 // });5096 });
5218 5097 workspace.update(cx, |workspace, cx| {
5219 // // Emit closed event on panel 2, which is not active5098 let pane = pane.read(cx);
5220 // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));5099 assert!(!pane.is_zoomed());
5221 5100 assert!(!pane.focus_handle(cx).is_focused(cx));
5222 // // Wo don't close the left dock, because panel_2 wasn't the active panel5101 assert!(workspace.right_dock().read(cx).is_open());
5223 // workspace.update(cx, |workspace, cx| {5102 assert!(workspace.zoomed.is_none());
5224 // let left_dock = workspace.left_dock();5103 });
5225 // assert!(left_dock.read(cx).is_open());5104 }
5226 // assert_eq!(5105
5227 // left_dock.read(cx).visible_panel().unwrap().panel_id(),5106 struct TestModal(FocusHandle);
5228 // panel_1.panel_id(),5107
5229 // );5108 impl TestModal {
5230 // });5109 fn new(cx: &mut ViewContext<Self>) -> Self {
5231 5110 Self(cx.focus_handle())
5232 // // Emitting a ZoomIn event shows the panel as zoomed.5111 }
5233 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));5112 }
5234 // workspace.update(cx, |workspace, _| {5113
5235 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));5114 impl EventEmitter<DismissEvent> for TestModal {}
5236 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));5115
5237 // });5116 impl FocusableView for TestModal {
5238 5117 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5239 // // Move panel to another dock while it is zoomed5118 self.0.clone()
5240 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));5119 }
5241 // workspace.update(cx, |workspace, _| {5120 }
5242 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));5121
5243 5122 impl ModalView for TestModal {}
5244 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));5123
5245 // });5124 impl Render for TestModal {
5246 5125 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5247 // // If focus is transferred to another view that's not a panel or another pane, we still show5126 div().track_focus(&self.0)
5248 // // the panel as zoomed.5127 }
5249 // let other_focus_handle = cx.update(|cx| cx.focus_handle());5128 }
5250 // cx.update(|cx| cx.focus(&other_focus_handle));5129
5251 // workspace.update(cx, |workspace, _| {5130 #[gpui::test]
5252 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));5131 async fn test_panels(cx: &mut gpui::TestAppContext) {
5253 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));5132 init_test(cx);
5254 // });5133 let fs = FakeFs::new(cx.executor());
5255 5134
5256 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.5135 let project = Project::test(fs, [], cx).await;
5257 // workspace.update(cx, |_, cx| cx.focus_self());5136 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5258 // workspace.update(cx, |workspace, _| {5137
5259 // assert_eq!(workspace.zoomed, None);5138 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5260 // assert_eq!(workspace.zoomed_position, None);5139 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5261 // });5140 workspace.add_panel(panel_1.clone(), cx);
5262 5141 workspace
5263 // // If focus is transferred again to another view that's not a panel or a pane, we won't5142 .left_dock()
5264 // // show the panel as zoomed because it wasn't zoomed before.5143 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5265 // cx.update(|cx| cx.focus(&other_focus_handle));5144 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5266 // workspace.update(cx, |workspace, _| {5145 workspace.add_panel(panel_2.clone(), cx);
5267 // assert_eq!(workspace.zoomed, None);5146 workspace
5268 // assert_eq!(workspace.zoomed_position, None);5147 .right_dock()
5269 // });5148 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5270 5149
5271 // // When focus is transferred back to the panel, it is zoomed again.5150 let left_dock = workspace.left_dock();
5272 // panel_1.update(cx, |_, cx| cx.focus_self());5151 assert_eq!(
5273 // workspace.update(cx, |workspace, _| {5152 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5274 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));5153 panel_1.panel_id()
5275 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));5154 );
5276 // });5155 assert_eq!(
5277 5156 left_dock.read(cx).active_panel_size(cx).unwrap(),
5278 // // Emitting a ZoomOut event unzooms the panel.5157 panel_1.size(cx)
5279 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));5158 );
5280 // workspace.update(cx, |workspace, _| {5159
5281 // assert_eq!(workspace.zoomed, None);5160 left_dock.update(cx, |left_dock, cx| {
5282 // assert_eq!(workspace.zoomed_position, None);5161 left_dock.resize_active_panel(Some(px(1337.)), cx)
5283 // });5162 });
5284 5163 assert_eq!(
5285 // // Emit closed event on panel 1, which is active5164 workspace
5286 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));5165 .right_dock()
5287 5166 .read(cx)
5288 // // Now the left dock is closed, because panel_1 was the active panel5167 .visible_panel()
5289 // workspace.update(cx, |workspace, cx| {5168 .unwrap()
5290 // let right_dock = workspace.right_dock();5169 .panel_id(),
5291 // assert!(!right_dock.read(cx).is_open());5170 panel_2.panel_id(),
5292 // });5171 );
5293 // }5172
5173 (panel_1, panel_2)
5174 });
5175
5176 // Move panel_1 to the right
5177 panel_1.update(cx, |panel_1, cx| {
5178 panel_1.set_position(DockPosition::Right, cx)
5179 });
5180
5181 workspace.update(cx, |workspace, cx| {
5182 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5183 // Since it was the only panel on the left, the left dock should now be closed.
5184 assert!(!workspace.left_dock().read(cx).is_open());
5185 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5186 let right_dock = workspace.right_dock();
5187 assert_eq!(
5188 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5189 panel_1.panel_id()
5190 );
5191 assert_eq!(
5192 right_dock.read(cx).active_panel_size(cx).unwrap(),
5193 px(1337.)
5194 );
5195
5196 // Now we move panel_2 to the left
5197 panel_2.set_position(DockPosition::Left, cx);
5198 });
5199
5200 workspace.update(cx, |workspace, cx| {
5201 // Since panel_2 was not visible on the right, we don't open the left dock.
5202 assert!(!workspace.left_dock().read(cx).is_open());
5203 // And the right dock is unaffected in it's displaying of panel_1
5204 assert!(workspace.right_dock().read(cx).is_open());
5205 assert_eq!(
5206 workspace
5207 .right_dock()
5208 .read(cx)
5209 .visible_panel()
5210 .unwrap()
5211 .panel_id(),
5212 panel_1.panel_id(),
5213 );
5214 });
5215
5216 // Move panel_1 back to the left
5217 panel_1.update(cx, |panel_1, cx| {
5218 panel_1.set_position(DockPosition::Left, cx)
5219 });
5220
5221 workspace.update(cx, |workspace, cx| {
5222 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5223 let left_dock = workspace.left_dock();
5224 assert!(left_dock.read(cx).is_open());
5225 assert_eq!(
5226 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5227 panel_1.panel_id()
5228 );
5229 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5230 // And the right dock should be closed as it no longer has any panels.
5231 assert!(!workspace.right_dock().read(cx).is_open());
5232
5233 // Now we move panel_1 to the bottom
5234 panel_1.set_position(DockPosition::Bottom, cx);
5235 });
5236
5237 workspace.update(cx, |workspace, cx| {
5238 // Since panel_1 was visible on the left, we close the left dock.
5239 assert!(!workspace.left_dock().read(cx).is_open());
5240 // The bottom dock is sized based on the panel's default size,
5241 // since the panel orientation changed from vertical to horizontal.
5242 let bottom_dock = workspace.bottom_dock();
5243 assert_eq!(
5244 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5245 panel_1.size(cx),
5246 );
5247 // Close bottom dock and move panel_1 back to the left.
5248 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5249 panel_1.set_position(DockPosition::Left, cx);
5250 });
5251
5252 // Emit activated event on panel 1
5253 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5254
5255 // Now the left dock is open and panel_1 is active and focused.
5256 workspace.update(cx, |workspace, cx| {
5257 let left_dock = workspace.left_dock();
5258 assert!(left_dock.read(cx).is_open());
5259 assert_eq!(
5260 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5261 panel_1.panel_id(),
5262 );
5263 assert!(panel_1.focus_handle(cx).is_focused(cx));
5264 });
5265
5266 // Emit closed event on panel 2, which is not active
5267 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5268
5269 // Wo don't close the left dock, because panel_2 wasn't the active panel
5270 workspace.update(cx, |workspace, cx| {
5271 let left_dock = workspace.left_dock();
5272 assert!(left_dock.read(cx).is_open());
5273 assert_eq!(
5274 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5275 panel_1.panel_id(),
5276 );
5277 });
5278
5279 // Emitting a ZoomIn event shows the panel as zoomed.
5280 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5281 workspace.update(cx, |workspace, _| {
5282 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5283 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5284 });
5285
5286 // Move panel to another dock while it is zoomed
5287 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5288 workspace.update(cx, |workspace, _| {
5289 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5290
5291 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5292 });
5293
5294 // This is a helper for getting a:
5295 // - valid focus on an element,
5296 // - that isn't a part of the panes and panels system of the Workspace,
5297 // - and doesn't trigger the 'on_focus_lost' API.
5298 let focus_other_view = {
5299 let workspace = workspace.clone();
5300 move |cx: &mut VisualTestContext| {
5301 workspace.update(cx, |workspace, cx| {
5302 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5303 workspace.toggle_modal(cx, TestModal::new);
5304 workspace.toggle_modal(cx, TestModal::new);
5305 } else {
5306 workspace.toggle_modal(cx, TestModal::new);
5307 }
5308 })
5309 }
5310 };
5311
5312 // If focus is transferred to another view that's not a panel or another pane, we still show
5313 // the panel as zoomed.
5314 focus_other_view(cx);
5315 workspace.update(cx, |workspace, _| {
5316 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5317 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5318 });
5319
5320 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5321 workspace.update(cx, |_, cx| cx.focus_self());
5322 workspace.update(cx, |workspace, _| {
5323 assert_eq!(workspace.zoomed, None);
5324 assert_eq!(workspace.zoomed_position, None);
5325 });
5326
5327 // If focus is transferred again to another view that's not a panel or a pane, we won't
5328 // show the panel as zoomed because it wasn't zoomed before.
5329 focus_other_view(cx);
5330 workspace.update(cx, |workspace, _| {
5331 assert_eq!(workspace.zoomed, None);
5332 assert_eq!(workspace.zoomed_position, None);
5333 });
5334
5335 // When the panel is activated, it is zoomed again.
5336 cx.dispatch_action(ToggleRightDock);
5337 workspace.update(cx, |workspace, _| {
5338 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5339 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5340 });
5341
5342 // Emitting a ZoomOut event unzooms the panel.
5343 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5344 workspace.update(cx, |workspace, _| {
5345 assert_eq!(workspace.zoomed, None);
5346 assert_eq!(workspace.zoomed_position, None);
5347 });
5348
5349 // Emit closed event on panel 1, which is active
5350 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5351
5352 // Now the left dock is closed, because panel_1 was the active panel
5353 workspace.update(cx, |workspace, cx| {
5354 let right_dock = workspace.right_dock();
5355 assert!(!right_dock.read(cx).is_open());
5356 });
5357 }
5294 5358
5295 pub fn init_test(cx: &mut TestAppContext) {5359 pub fn init_test(cx: &mut TestAppContext) {
5296 cx.update(|cx| {5360 cx.update(|cx| {
crates/zed/src/zed.rs+1−0
@@ -148,6 +148,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) { · 147 unmodified lines
148 cx.on_window_should_close(move |cx| {148 cx.on_window_should_close(move |cx| {
149 handle149 handle
150 .update(cx, |workspace, cx| {150 .update(cx, |workspace, cx| {
151 // We'll handle closing asynchoronously
151 workspace.close_window(&Default::default(), cx);152 workspace.close_window(&Default::default(), cx);
152 false153 false
153 })154 })
docs/old/tools.md+1−1
@@ -56,7 +56,7 @@ We use Vercel for all of our web deployments and some backend things. If you sig · 55 unmodified lines
56 56
57### Environment Variables57### Environment Variables
58 58
59You can get access to many of our shared enviroment variables through 1Password and Vercel. For 1Password search the value you are looking for, or sort by passwords or API credentials.59You can get access to many of our shared environment variables through 1Password and Vercel. For 1Password search the value you are looking for, or sort by passwords or API credentials.
60 60
61For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env).61For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env).
62 62
rust-toolchain.toml+2−1
@@ -1,4 +1,5 @@
1[toolchain]1[toolchain]
2channel = "1.75"2channel = "1.75"
3components = [ "rustfmt" ]3profile = "minimal"
4components = [ "rustfmt", "clippy" ]
4targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]5targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]
script/zed-local+36−11
@@ -1,24 +1,45 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2 2
3const HELP = `
4USAGE
5 zed-local [options] [zed args]
6
7OPTIONS
8 --help Print this help message
9 --release Build Zed in release mode
10 -2, -3, -4 Spawn 2, 3, or 4 Zed instances, with their windows tiled.
11 --top Arrange the Zed windows so they take up the top half of the screen.
12`.trim();
13
3const { spawn, execFileSync } = require("child_process");14const { spawn, execFileSync } = require("child_process");
4 15
5const RESOLUTION_REGEX = /(\d+) x (\d+)/;16const RESOLUTION_REGEX = /(\d+) x (\d+)/;
6const DIGIT_FLAG_REGEX = /^--?(\d+)$/;17const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
7const RELEASE_MODE = "--release";18
19let instanceCount = 1;
20let isReleaseMode = false;
21let isTop = false;
8 22
9const args = process.argv.slice(2);23const args = process.argv.slice(2);
24while (args.length > 0) {
25 const arg = args[0];
26
27 const digitMatch = arg.match(DIGIT_FLAG_REGEX);
28 if (digitMatch) {
29 instanceCount = parseInt(digitMatch[1]);
30 } else if (arg === "--release") {
31 isReleaseMode = true;
32 } else if (arg === "--top") {
33 isTop = true;
34 } else if (arg === "--help") {
35 console.log(HELP);
36 process.exit(0);
37 } else {
38 break;
39 }
10 40
11// Parse the number of Zed instances to spawn.
12let instanceCount = 1;
13const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX);
14if (digitMatch) {
15 instanceCount = parseInt(digitMatch[1]);
16 args.shift();41 args.shift();
17}42}
18const isReleaseMode = args.some((arg) => arg === RELEASE_MODE);
19if (instanceCount > 4) {
20 throw new Error("Cannot spawn more than 4 instances");
21}
22 43
23// Parse the resolution of the main screen44// Parse the resolution of the main screen
24const displayInfo = JSON.parse(45const displayInfo = JSON.parse(
@@ -34,7 +55,11 @@ if (!mainDisplayResolution) { · 9 unmodified lines
34 throw new Error("Could not parse screen resolution");55 throw new Error("Could not parse screen resolution");
35}56}
36const screenWidth = parseInt(mainDisplayResolution[1]);57const screenWidth = parseInt(mainDisplayResolution[1]);
37const screenHeight = parseInt(mainDisplayResolution[2]);58let screenHeight = parseInt(mainDisplayResolution[2]);
59
60if (isTop) {
61 screenHeight = Math.floor(screenHeight / 2);
62}
38 63
39// Determine the window size for each instance64// Determine the window size for each instance
40let instanceWidth = screenWidth;65let instanceWidth = screenWidth;