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 @@
1−name: 'Check formatting'
2−description: 'Checks code formatting use cargo fmt'
3−
4−runs:
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 @@
1+name: "Check formatting"
2+description: "Checks code formatting use cargo fmt"
3+
4+runs:
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
22 description: "Runs the tests"
33
44 runs:
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− rustup target add wasm32-wasi
13− cargo install cargo-nextest
5+ using: "composite"
6+ steps:
7+ - name: Install Rust
8+ shell: bash -euxo pipefail {0}
9+ run: |
10+ cargo install cargo-nextest
1411
15− - name: Install Node
16− uses: actions/setup-node@v3
17− with:
18− node-version: "18"
12+ - name: Install Node
13+ uses: actions/setup-node@v3
14+ with:
15+ node-version: "18"
1916
20− - name: Limit target directory size
21− shell: bash -euxo pipefail {0}
22− run: script/clear-target-dir-if-larger-than 100
17+ - name: Limit target directory size
18+ shell: bash -euxo pipefail {0}
19+ run: script/clear-target-dir-if-larger-than 100
2320
24− - name: Run check
25− shell: bash -euxo pipefail {0}
26− run: cargo check --tests --workspace
21+ - name: Run check
22+ shell: bash -euxo pipefail {0}
23+ run: cargo check --tests --workspace
2724
28− - name: Run tests
29− shell: bash -euxo pipefail {0}
30− run: cargo nextest run --workspace --no-fail-fast
25+ - name: Run tests
26+ shell: bash -euxo pipefail {0}
27+ run: cargo nextest run --workspace --no-fail-fast
.github/workflows/ci.yml+6−13
@@ -22,8 +22,8 @@ env: · 21 unmodified lines
2222 RUST_BACKTRACE: 1
2323
2424 jobs:
25− rustfmt:
26− name: Check formatting
25+ style:
26+ name: Check formatting and Clippy lints
2727 runs-on:
2828 - self-hosted
2929 - test
@@ -33,19 +33,20 @@ jobs: · 3 unmodified lines
3333 with:
3434 clean: false
3535 submodules: "recursive"
36+ fetch-depth: 0
3637
3738 - name: Set up default .cargo/config.toml
3839 run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
3940
40− - name: Run rustfmt
41− uses: ./.github/actions/check_formatting
41+ - name: Run style checks
42+ uses: ./.github/actions/check_style
4243
4344 tests:
4445 name: Run tests
4546 runs-on:
4647 - self-hosted
4748 - test
48− needs: rustfmt
49+ needs: style
4950 steps:
5051 - name: Checkout repo
5152 uses: actions/checkout@v3
@@ -75,14 +76,6 @@ jobs: · 23 unmodified lines
7576 APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
7677 APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
7778 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−
8679 - name: Install Node
8780 uses: actions/setup-node@v3
8881 with:
.github/workflows/randomized_tests.yml+27−32
@@ -3,41 +3,36 @@ name: Randomized Tests · 2 unmodified lines
33 concurrency: randomized-tests
44
55 on:
6− push:
7− branches:
8− - randomized-tests-runner
9− # schedule:
10− # - cron: '0 * * * *'
6+ push:
7+ branches:
8+ - randomized-tests-runner
9+ # schedule:
10+ # - cron: '0 * * * *'
1111
1212 env:
13− CARGO_TERM_COLOR: always
14− CARGO_INCREMENTAL: 0
15− RUST_BACKTRACE: 1
16− ZED_SERVER_URL: https://zed.dev
17− ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
13+ CARGO_TERM_COLOR: always
14+ CARGO_INCREMENTAL: 0
15+ RUST_BACKTRACE: 1
16+ ZED_SERVER_URL: https://zed.dev
17+ ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
1818
1919 jobs:
20− tests:
21− name: Run randomized tests
22− runs-on:
23− - self-hosted
24− - randomized-tests
25− steps:
26− - name: Install Rust
27− run: |
28− rustup set profile minimal
29− rustup update stable
20+ tests:
21+ name: Run randomized tests
22+ runs-on:
23+ - self-hosted
24+ - randomized-tests
25+ steps:
26+ - name: Install Node
27+ uses: actions/setup-node@v3
28+ with:
29+ node-version: "18"
3030
31− - name: Install Node
32− uses: actions/setup-node@v3
33− with:
34− node-version: '18'
31+ - name: Checkout repo
32+ uses: actions/checkout@v3
33+ with:
34+ clean: false
35+ submodules: "recursive"
3536
36− - name: Checkout repo
37− uses: actions/checkout@v3
38− with:
39− clean: false
40− submodules: 'recursive'
41−
42− - name: Run randomized tests
43− run: script/randomized-test-ci
37+ - name: Run randomized tests
38+ run: script/randomized-test-ci
.github/workflows/release_nightly.yml+6−13
@@ -14,8 +14,8 @@ env: · 13 unmodified lines
1414 RUST_BACKTRACE: 1
1515
1616 jobs:
17− rustfmt:
18− name: Check formatting
17+ style:
18+ name: Check formatting and Clippy lints
1919 runs-on:
2020 - self-hosted
2121 - test
@@ -25,16 +25,17 @@ jobs: · 3 unmodified lines
2525 with:
2626 clean: false
2727 submodules: "recursive"
28+ fetch-depth: 0
2829
29− - name: Run rustfmt
30− uses: ./.github/actions/check_formatting
30+ - name: Run style checks
31+ uses: ./.github/actions/check_style
3132
3233 tests:
3334 name: Run tests
3435 runs-on:
3536 - self-hosted
3637 - test
37− needs: rustfmt
38+ needs: style
3839 steps:
3940 - name: Checkout repo
4041 uses: actions/checkout@v3
@@ -59,14 +60,6 @@ jobs: · 18 unmodified lines
5960 DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
6061 DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
6162 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−
7063 - name: Install Node
7164 uses: actions/setup-node@v3
7265 with:
.zed/settings.json+2−1
@@ -1,5 +1,6 @@
11 {
22 "JSON": {
33 "tab_size": 4
4− }
4+ },
5+ "formatter": "auto"
56 }
Cargo.lock+2−68
@@ -1452,7 +1452,7 @@ dependencies = [ · 1451 unmodified lines
14521452
14531453 [[package]]
14541454 name = "collab"
1455−version = "0.36.0"
1455+version = "0.36.1"
14561456 dependencies = [
14571457 "anyhow",
14581458 "async-trait",
@@ -2522,6 +2522,7 @@ dependencies = [ · 1063 unmodified lines
25222522 name = "file_finder"
25232523 version = "0.1.0"
25242524 dependencies = [
2525+ "anyhow",
25252526 "collections",
25262527 "ctor",
25272528 "editor",
@@ -3440,40 +3441,6 @@ dependencies = [ · 912 unmodified lines
34403441 "tiff",
34413442 ]
34423443
3443−[[package]]
3444−name = "include-flate"
3445−version = "0.2.0"
3446−source = "registry+https://github.com/rust-lang/crates.io-index"
3447−checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0"
3448−dependencies = [
3449− "include-flate-codegen-exports",
3450− "lazy_static",
3451− "libflate",
3452−]
3453−
3454−[[package]]
3455−name = "include-flate-codegen"
3456−version = "0.1.4"
3457−source = "registry+https://github.com/rust-lang/crates.io-index"
3458−checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8"
3459−dependencies = [
3460− "libflate",
3461− "proc-macro-hack",
3462− "proc-macro2",
3463− "quote",
3464− "syn 1.0.109",
3465−]
3466−
3467−[[package]]
3468−name = "include-flate-codegen-exports"
3469−version = "0.1.4"
3470−source = "registry+https://github.com/rust-lang/crates.io-index"
3471−checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03"
3472−dependencies = [
3473− "include-flate-codegen",
3474− "proc-macro-hack",
3475−]
3476−
34773444 [[package]]
34783445 name = "indexmap"
34793446 version = "1.9.3"
@@ -3865,26 +3832,6 @@ version = "0.2.148" · 385 unmodified lines
38653832 source = "registry+https://github.com/rust-lang/crates.io-index"
38663833 checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
38673834
3868−[[package]]
3869−name = "libflate"
3870−version = "1.4.0"
3871−source = "registry+https://github.com/rust-lang/crates.io-index"
3872−checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
3873−dependencies = [
3874− "adler32",
3875− "crc32fast",
3876− "libflate_lz77",
3877−]
3878−
3879−[[package]]
3880−name = "libflate_lz77"
3881−version = "1.2.0"
3882−source = "registry+https://github.com/rust-lang/crates.io-index"
3883−checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
3884−dependencies = [
3885− "rle-decode-fast",
3886−]
3887−
38883835 [[package]]
38893836 name = "libgit2-sys"
38903837 version = "0.14.2+1.5.1"
@@ -5462,12 +5409,6 @@ dependencies = [ · 1571 unmodified lines
54625409 "version_check",
54635410 ]
54645411
5465−[[package]]
5466−name = "proc-macro-hack"
5467−version = "0.5.20+deprecated"
5468−source = "registry+https://github.com/rust-lang/crates.io-index"
5469−checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
5470−
54715412 [[package]]
54725413 name = "proc-macro2"
54735414 version = "1.0.67"
@@ -6162,12 +6103,6 @@ dependencies = [ · 688 unmodified lines
61626103 "syn 1.0.109",
61636104 ]
61646105
6165−[[package]]
6166−name = "rle-decode-fast"
6167−version = "1.0.3"
6168−source = "registry+https://github.com/rust-lang/crates.io-index"
6169−checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
6170−
61716106 [[package]]
61726107 name = "rmp"
61736108 version = "0.8.12"
@@ -6315,7 +6250,6 @@ version = "8.0.0" · 141 unmodified lines
63156250 source = "registry+https://github.com/rust-lang/crates.io-index"
63166251 checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
63176252 dependencies = [
6318− "include-flate",
63196253 "rust-embed-impl",
63206254 "rust-embed-utils",
63216255 "walkdir",
Cargo.toml+1−1
@@ -110,7 +110,7 @@ prost = { version = "0.8" } · 109 unmodified lines
110110 rand = { version = "0.8.5" }
111111 refineable = { path = "./crates/refineable" }
112112 regex = { version = "1.5" }
113−rust-embed = { version = "8.0", features = ["include-exclude", "compression"] }
113+rust-embed = { version = "8.0", features = ["include-exclude"] }
114114 rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
115115 schemars = { version = "0.8" }
116116 serde = { version = "1.0", features = ["derive", "rc"] }
assets/keymaps/default.json+2−1
@@ -412,7 +412,8 @@ · 411 unmodified lines
412412 "cmd-shift-e": "project_panel::ToggleFocus",
413413 "cmd-?": "assistant::ToggleFocus",
414414 "cmd-alt-s": "workspace::SaveAll",
415− "cmd-k m": "language_selector::Toggle"
415+ "cmd-k m": "language_selector::Toggle",
416+ "escape": "workspace::Unfollow"
416417 }
417418 },
418419 // Bindings from Sublime Text
crates/call/src/call.rs+3−2
@@ -239,7 +239,8 @@ impl ActiveCall { · 238 unmodified lines
239239 if result.is_ok() {
240240 this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
241241 } else {
242− // TODO: Resport collaboration error
242+ //TODO: report collaboration error
243+ log::error!("invite failed: {:?}", result);
243244 }
244245
245246 this.update(&mut cx, |this, cx| {
@@ -282,7 +283,7 @@ impl ActiveCall { · 36 unmodified lines
282283 return Task::ready(Err(anyhow!("cannot join while on another call")));
283284 }
284285
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() {
286287 call
287288 } else {
288289 return Task::ready(Err(anyhow!("no incoming call")));
crates/call/src/room.rs+52−57
@@ -15,10 +15,7 @@ use gpui::{ · 14 unmodified lines
1515 AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
1616 };
1717 use language::LanguageRegistry;
18−use live_kit_client::{
19− LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
20− RemoteVideoTrackUpdate,
21−};
18+use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
2219 use postage::{sink::Sink, stream::Stream, watch};
2320 use project::Project;
2421 use settings::Settings as _;
@@ -131,30 +128,11 @@ impl Room { · 106 unmodified lines
131128 }
132129 });
133130
134− let _maintain_video_tracks = cx.spawn({
131+ let _handle_updates = cx.spawn({
135132 let room = room.clone();
136133 move |this, mut cx| async move {
137− let mut track_video_changes = room.remote_video_track_updates();
138− while let Some(track_change) = track_video_changes.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 {
134+ let mut updates = room.updates();
135+ while let Some(update) = updates.next().await {
158136 let this = if let Some(this) = this.upgrade() {
159137 this
160138 } else {
@@ -162,7 +140,7 @@ impl Room { · 1 unmodified line
162140 };
163141
164142 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()
166144 })
167145 .ok();
168146 }
@@ -195,7 +173,7 @@ impl Room { · 26 unmodified lines
195173 deafened: false,
196174 speaking: false,
197175 _maintain_room,
198− _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
176+ _handle_updates,
199177 })
200178 } else {
201179 None
@@ -877,8 +855,8 @@ impl Room { · 675 unmodified lines
877855 .remote_audio_track_publications(&user.id.to_string());
878856
879857 for track in video_tracks {
880− this.remote_video_track_updated(
881− RemoteVideoTrackUpdate::Subscribed(track),
858+ this.live_kit_room_updated(
859+ RoomUpdate::SubscribedToRemoteVideoTrack(track),
882860 cx,
883861 )
884862 .log_err();
@@ -887,8 +865,8 @@ impl Room { · 2 unmodified lines
887865 for (track, publication) in
888866 audio_tracks.iter().zip(publications.iter())
889867 {
890− this.remote_audio_track_updated(
891− RemoteAudioTrackUpdate::Subscribed(
868+ this.live_kit_room_updated(
869+ RoomUpdate::SubscribedToRemoteAudioTrack(
892870 track.clone(),
893871 publication.clone(),
894872 ),
@@ -979,13 +957,13 @@ impl Room { · 84 unmodified lines
979957 }
980958 }
981959
982− fn remote_video_track_updated(
960+ fn live_kit_room_updated(
983961 &mut self,
984− change: RemoteVideoTrackUpdate,
962+ update: RoomUpdate,
985963 cx: &mut ModelContext<Self>,
986964 ) -> Result<()> {
987− match change {
988− RemoteVideoTrackUpdate::Subscribed(track) => {
965+ match update {
966+ RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
989967 let user_id = track.publisher_id().parse()?;
990968 let track_id = track.sid().to_string();
991969 let participant = self
@@ -997,7 +975,8 @@ impl Room { · 5 unmodified lines
997975 participant_id: participant.peer_id,
998976 });
999977 }
1000− RemoteVideoTrackUpdate::Unsubscribed {
978+
979+ RoomUpdate::UnsubscribedFromRemoteVideoTrack {
1001980 publisher_id,
1002981 track_id,
1003982 } => {
@@ -1011,19 +990,8 @@ impl Room { · 7 unmodified lines
1011990 participant_id: participant.peer_id,
1012991 });
1013992 }
1014− }
1015993
1016− cx.notify();
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 } => {
994+ RoomUpdate::ActiveSpeakersChanged { speakers } => {
1027995 let mut speaker_ids = speakers
1028996 .into_iter()
1029997 .filter_map(|speaker_sid| speaker_sid.parse().ok())
@@ -1045,9 +1013,9 @@ impl Room { · 15 unmodified lines
10451013 }
10461014 }
10471015 }
1048− cx.notify();
10491016 }
1050− RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
1017+
1018+ RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } => {
10511019 let mut found = false;
10521020 for participant in &mut self.remote_participants.values_mut() {
10531021 for track in participant.audio_tracks.values() {
@@ -1061,10 +1029,9 @@ impl Room { · 7 unmodified lines
10611029 break;
10621030 }
10631031 }
1064−
1065− cx.notify();
10661032 }
1067− RemoteAudioTrackUpdate::Subscribed(track, publication) => {
1033+
1034+ RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
10681035 let user_id = track.publisher_id().parse()?;
10691036 let track_id = track.sid().to_string();
10701037 let participant = self
@@ -1078,7 +1045,8 @@ impl Room { · 7 unmodified lines
10781045 participant_id: participant.peer_id,
10791046 });
10801047 }
1081− RemoteAudioTrackUpdate::Unsubscribed {
1048+
1049+ RoomUpdate::UnsubscribedFromRemoteAudioTrack {
10821050 publisher_id,
10831051 track_id,
10841052 } => {
@@ -1092,6 +1060,28 @@ impl Room { · 7 unmodified lines
10921060 participant_id: participant.peer_id,
10931061 });
10941062 }
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+ }
10951085 }
10961086
10971087 cx.notify();
@@ -1235,7 +1225,12 @@ impl Room { · 137 unmodified lines
12351225 };
12361226
12371227 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(())
12391234 }
12401235
12411236 pub(crate) fn set_location(
@@ -1597,7 +1592,7 @@ struct LiveKitRoom { · 355 unmodified lines
15971592 speaking: bool,
15981593 next_publish_id: usize,
15991594 _maintain_room: Task<()>,
1600− _maintain_tracks: [Task<()>; 2],
1595+ _handle_updates: Task<()>,
16011596 }
16021597
16031598 impl LiveKitRoom {
crates/client/src/client.rs+2−5
@@ -1371,10 +1371,7 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> { · 1370 unmodified lines
13711371 })
13721372 }
13731373
1374−async fn write_credentials_to_keychain(
1375− credentials: Credentials,
1376− cx: &AsyncAppContext,
1377−) -> Result<()> {
1374+fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext) -> Result<()> {
13781375 cx.update(move |cx| {
13791376 cx.write_credentials(
13801377 &ZED_SERVER_URL,
@@ -1384,7 +1381,7 @@ async fn write_credentials_to_keychain( · 3 unmodified lines
13841381 })?
13851382 }
13861383
1387−async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
1384+fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
13881385 cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
13891386 }
13901387
crates/client/src/telemetry.rs+37−8
@@ -1,3 +1,5 @@
1+mod event_coalescer;
2+
13 use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
24 use chrono::{DateTime, Utc};
35 use futures::Future;
@@ -5,7 +7,6 @@ use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; · 1 unmodified line
57 use lazy_static::lazy_static;
68 use parking_lot::Mutex;
79 use serde::Serialize;
8−use serde_json;
910 use settings::{Settings, SettingsStore};
1011 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
1112 use sysinfo::{
@@ -15,6 +16,8 @@ use tempfile::NamedTempFile; · 3 unmodified lines
1516 use util::http::HttpClient;
1617 use util::{channel::ReleaseChannel, TryFutureExt};
1718
19+use self::event_coalescer::EventCoalescer;
20+
1821 pub struct Telemetry {
1922 http_client: Arc<dyn HttpClient>,
2023 executor: BackgroundExecutor,
@@ -34,6 +37,7 @@ struct TelemetryState { · 13 unmodified lines
3437 log_file: Option<NamedTempFile>,
3538 is_staff: Option<bool>,
3639 first_event_datetime: Option<DateTime<Utc>>,
40+ event_coalescer: EventCoalescer,
3741 }
3842
3943 const EVENTS_URL_PATH: &'static str = "/api/events";
@@ -118,19 +122,24 @@ pub enum Event { · 78 unmodified lines
118122 value: String,
119123 milliseconds_since_first_event: i64,
120124 },
125+ Edit {
126+ duration: i64,
127+ environment: &'static str,
128+ milliseconds_since_first_event: i64,
129+ },
121130 }
122131
123132 #[cfg(debug_assertions)]
124−const MAX_QUEUE_LEN: usize = 1;
133+const MAX_QUEUE_LEN: usize = 5;
125134
126135 #[cfg(not(debug_assertions))]
127136 const MAX_QUEUE_LEN: usize = 50;
128137
129138 #[cfg(debug_assertions)]
130−const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
139+const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
131140
132141 #[cfg(not(debug_assertions))]
133−const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
142+const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
134143
135144 impl Telemetry {
136145 pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
@@ -150,11 +159,12 @@ impl Telemetry { · 13 unmodified lines
150159 installation_id: None,
151160 metrics_id: None,
152161 session_id: None,
153− events_queue: Default::default(),
154− flush_events_task: Default::default(),
162+ events_queue: Vec::new(),
163+ flush_events_task: None,
155164 log_file: None,
156165 is_staff: None,
157166 first_event_datetime: None,
167+ event_coalescer: EventCoalescer::new(),
158168 }));
159169
160170 cx.observe_global::<SettingsStore>({
@@ -194,7 +204,7 @@ impl Telemetry { · 33 unmodified lines
194204 #[cfg(not(any(test, feature = "test-support")))]
195205 fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
196206 self.report_app_event("close");
197− self.flush_events();
207+ // TODO: close final edit period and make sure it's sent
198208 Task::ready(())
199209 }
200210
@@ -392,6 +402,22 @@ impl Telemetry { · 191 unmodified lines
392402 }
393403 }
394404
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+
395421 fn report_event(self: &Arc<Self>, event: Event) {
396422 let mut state = self.state.lock();
397423
@@ -410,7 +436,7 @@ impl Telemetry { · 12 unmodified lines
410436 let this = self.clone();
411437 let executor = self.executor.clone();
412438 state.flush_events_task = Some(self.executor.spawn(async move {
413− executor.timer(DEBOUNCE_INTERVAL).await;
439+ executor.timer(FLUSH_DEBOUNCE_INTERVAL).await;
414440 this.flush_events();
415441 }));
416442 }
@@ -435,6 +461,9 @@ impl Telemetry { · 18 unmodified lines
435461 let mut events = mem::take(&mut state.events_queue);
436462 state.flush_events_task.take();
437463 drop(state);
464+ if events.is_empty() {
465+ return;
466+ }
438467
439468 let this = self.clone();
440469 self.executor
crates/client/src/telemetry/event_coalescer.rs+279−0
@@ -0,0 +1,279 @@
1+use chrono::{DateTime, Duration, Utc};
2+use std::time;
3+
4+const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
5+const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1);
6+
7+#[derive(Debug, PartialEq)]
8+struct PeriodData {
9+ environment: &'static str,
10+ start: DateTime<Utc>,
11+ end: Option<DateTime<Utc>>,
12+}
13+
14+pub struct EventCoalescer {
15+ state: Option<PeriodData>,
16+}
17+
18+impl 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)]
79+mod 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
33 default-run = "collab"
44 edition = "2021"
55 name = "collab"
6−version = "0.36.0"
6+version = "0.36.1"
77 publish = false
88
99 [[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
3737 CREATE TABLE "rooms" (
3838 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
3939 "live_kit_room" VARCHAR NOT NULL,
40− "enviroment" VARCHAR,
40+ "environment" VARCHAR,
4141 "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
4242 );
4343 CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
crates/collab/migrations/20240111085546_fix_column_name.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE rooms ADD COLUMN environment TEXT;
crates/collab/src/db/queries/channels.rs+2−2
@@ -1180,7 +1180,7 @@ impl Database { · 1179 unmodified lines
11801180 .await?;
11811181
11821182 let room_id = if let Some(room) = room {
1183− if let Some(env) = room.enviroment {
1183+ if let Some(env) = room.environment {
11841184 if &env != environment {
11851185 Err(anyhow!("must join using the {} release", env))?;
11861186 }
@@ -1190,7 +1190,7 @@ impl Database { · 3 unmodified lines
11901190 let result = room::Entity::insert(room::ActiveModel {
11911191 channel_id: ActiveValue::Set(Some(channel_id)),
11921192 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())),
11941194 ..Default::default()
11951195 })
11961196 .exec(&*tx)
crates/collab/src/db/queries/rooms.rs+7−7
@@ -112,7 +112,7 @@ impl Database { · 111 unmodified lines
112112 self.transaction(|tx| async move {
113113 let room = room::ActiveModel {
114114 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())),
116116 ..Default::default()
117117 }
118118 .insert(&*tx)
@@ -299,28 +299,28 @@ impl Database { · 180 unmodified lines
299299 room_id: RoomId,
300300 user_id: UserId,
301301 connection: ConnectionId,
302− enviroment: &str,
302+ environment: &str,
303303 ) -> Result<RoomGuard<JoinRoom>> {
304304 self.room_transaction(room_id, |tx| async move {
305305 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
306− enum QueryChannelIdAndEnviroment {
306+ enum QueryChannelIdAndEnvironment {
307307 ChannelId,
308− Enviroment,
308+ Environment,
309309 }
310310
311311 let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
312312 room::Entity::find()
313313 .select_only()
314314 .column(room::Column::ChannelId)
315− .column(room::Column::Enviroment)
315+ .column(room::Column::Environment)
316316 .filter(room::Column::Id.eq(room_id))
317− .into_values::<_, QueryChannelIdAndEnviroment>()
317+ .into_values::<_, QueryChannelIdAndEnvironment>()
318318 .one(&*tx)
319319 .await?
320320 .ok_or_else(|| anyhow!("no such room"))?;
321321
322322 if let Some(release_channel) = release_channel {
323− if &release_channel != enviroment {
323+ if &release_channel != environment {
324324 Err(anyhow!("must join using the {} release", release_channel))?;
325325 }
326326 }
crates/collab/src/db/tables/room.rs+1−1
@@ -8,7 +8,7 @@ pub struct Model { · 7 unmodified lines
88 pub id: RoomId,
99 pub live_kit_room: String,
1010 pub channel_id: Option<ChannelId>,
11− pub enviroment: Option<String>,
11+ pub environment: Option<String>,
1212 }
1313
1414 #[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
599599 });
600600 }
601601
602−#[gpui::test(iterations = 10)]
603−async 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−
748602 #[gpui::test]
749603 async fn test_channel_buffer_changes(
750604 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
104104 });
105105 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
106106 assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
107− assert!(dbg!(
108− room_b
109− .update(cx_b, |room, cx| room.share_microphone(cx))
110− .await
111− )
112− .is_err());
107+ assert!(room_b
108+ .update(cx_b, |room, cx| room.share_microphone(cx))
109+ .await
110+ .is_err());
113111
114112 // B is promoted
115113 active_call_a
crates/collab/src/tests/channel_message_tests.rs+134−132
@@ -1,7 +1,9 @@
11 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
22 use channel::{ChannelChat, ChannelMessageId, MessageParams};
3+use collab_ui::chat_panel::ChatPanel;
34 use gpui::{BackgroundExecutor, Model, TestAppContext};
45 use rpc::Notification;
6+use workspace::dock::Panel;
57
68 #[gpui::test]
79 async fn test_basic_channel_messages(
@@ -273,135 +275,135 @@ fn assert_messages(chat: &Model<ChannelChat>, messages: &[&str], cx: &mut TestAp · 265 unmodified lines
273275 );
274276 }
275277
276−//todo!(collab_ui)
277−// #[gpui::test]
278−// async fn test_channel_message_changes(
279−// executor: BackgroundExecutor,
280−// cx_a: &mut TestAppContext,
281−// cx_b: &mut TestAppContext,
282−// ) {
283−// let mut server = TestServer::start(&executor).await;
284−// let client_a = server.create_client(cx_a, "user_a").await;
285−// let client_b = server.create_client(cx_b, "user_b").await;
286−
287−// let channel_id = server
288−// .make_channel(
289−// "the-channel",
290−// None,
291−// (&client_a, cx_a),
292−// &mut [(&client_b, cx_b)],
293−// )
294−// .await;
295−
296−// // Client A sends a message, client B should see that there is a new message.
297−// let channel_chat_a = client_a
298−// .channel_store()
299−// .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
300−// .await
301−// .unwrap();
302−
303−// channel_chat_a
304−// .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
305−// .await
306−// .unwrap();
307−
308−// executor.run_until_parked();
309−
310−// let b_has_messages = cx_b.read_with(|cx| {
311−// client_b
312−// .channel_store()
313−// .read(cx)
314−// .has_new_messages(channel_id)
315−// .unwrap()
316−// });
317−
318−// assert!(b_has_messages);
319−
320−// // Opening the chat should clear the changed flag.
321−// cx_b.update(|cx| {
322−// collab_ui::init(&client_b.app_state, cx);
323−// });
324−// let project_b = client_b.build_empty_local_project(cx_b);
325−// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
326−// let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
327−// chat_panel_b
328−// .update(cx_b, |chat_panel, cx| {
329−// chat_panel.set_active(true, cx);
330−// chat_panel.select_channel(channel_id, None, cx)
331−// })
332−// .await
333−// .unwrap();
334−
335−// executor.run_until_parked();
336−
337−// let b_has_messages = cx_b.read_with(|cx| {
338−// client_b
339−// .channel_store()
340−// .read(cx)
341−// .has_new_messages(channel_id)
342−// .unwrap()
343−// });
344−
345−// assert!(!b_has_messages);
346−
347−// // Sending a message while the chat is open should not change the flag.
348−// channel_chat_a
349−// .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
350−// .await
351−// .unwrap();
352−
353−// executor.run_until_parked();
354−
355−// let b_has_messages = cx_b.read_with(|cx| {
356−// client_b
357−// .channel_store()
358−// .read(cx)
359−// .has_new_messages(channel_id)
360−// .unwrap()
361−// });
362−
363−// assert!(!b_has_messages);
364−
365−// // Sending a message while the chat is closed should change the flag.
366−// chat_panel_b.update(cx_b, |chat_panel, cx| {
367−// chat_panel.set_active(false, cx);
368−// });
369−
370−// // Sending a message while the chat is open should not change the flag.
371−// channel_chat_a
372−// .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
373−// .await
374−// .unwrap();
375−
376−// executor.run_until_parked();
377−
378−// let b_has_messages = cx_b.read_with(|cx| {
379−// client_b
380−// .channel_store()
381−// .read(cx)
382−// .has_new_messages(channel_id)
383−// .unwrap()
384−// });
385−
386−// assert!(b_has_messages);
387−
388−// // Closing the chat should re-enable change tracking
389−// cx_b.update(|_| drop(chat_panel_b));
390−
391−// channel_chat_a
392−// .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
393−// .await
394−// .unwrap();
395−
396−// executor.run_until_parked();
397−
398−// let b_has_messages = cx_b.read_with(|cx| {
399−// client_b
400−// .channel_store()
401−// .read(cx)
402−// .has_new_messages(channel_id)
403−// .unwrap()
404−// });
405−
406−// assert!(b_has_messages);
407−// }
278+#[gpui::test]
279+async fn test_channel_message_changes(
280+ executor: BackgroundExecutor,
281+ cx_a: &mut TestAppContext,
282+ cx_b: &mut TestAppContext,
283+) {
284+ let mut server = TestServer::start(executor.clone()).await;
285+ let client_a = server.create_client(cx_a, "user_a").await;
286+ let client_b = server.create_client(cx_b, "user_b").await;
287+
288+ let channel_id = server
289+ .make_channel(
290+ "the-channel",
291+ None,
292+ (&client_a, cx_a),
293+ &mut [(&client_b, cx_b)],
294+ )
295+ .await;
296+
297+ // Client A sends a message, client B should see that there is a new message.
298+ let channel_chat_a = client_a
299+ .channel_store()
300+ .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
301+ .await
302+ .unwrap();
303+
304+ channel_chat_a
305+ .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
306+ .await
307+ .unwrap();
308+
309+ executor.run_until_parked();
310+
311+ let b_has_messages = cx_b.update(|cx| {
312+ client_b
313+ .channel_store()
314+ .read(cx)
315+ .has_new_messages(channel_id)
316+ .unwrap()
317+ });
318+
319+ assert!(b_has_messages);
320+
321+ // Opening the chat should clear the changed flag.
322+ cx_b.update(|cx| {
323+ collab_ui::init(&client_b.app_state, cx);
324+ });
325+ 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);
327+
328+ let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
329+ chat_panel_b
330+ .update(cx_b, |chat_panel, cx| {
331+ chat_panel.set_active(true, cx);
332+ chat_panel.select_channel(channel_id, None, cx)
333+ })
334+ .await
335+ .unwrap();
336+
337+ executor.run_until_parked();
338+
339+ let b_has_messages = cx_b.update(|cx| {
340+ client_b
341+ .channel_store()
342+ .read(cx)
343+ .has_new_messages(channel_id)
344+ .unwrap()
345+ });
346+
347+ assert!(!b_has_messages);
348+
349+ // Sending a message while the chat is open should not change the flag.
350+ channel_chat_a
351+ .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
352+ .await
353+ .unwrap();
354+
355+ executor.run_until_parked();
356+
357+ let b_has_messages = cx_b.update(|cx| {
358+ client_b
359+ .channel_store()
360+ .read(cx)
361+ .has_new_messages(channel_id)
362+ .unwrap()
363+ });
364+
365+ assert!(!b_has_messages);
366+
367+ // Sending a message while the chat is closed should change the flag.
368+ chat_panel_b.update(cx_b, |chat_panel, cx| {
369+ chat_panel.set_active(false, cx);
370+ });
371+
372+ // Sending a message while the chat is open should not change the flag.
373+ channel_chat_a
374+ .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
375+ .await
376+ .unwrap();
377+
378+ executor.run_until_parked();
379+
380+ let b_has_messages = cx_b.update(|cx| {
381+ client_b
382+ .channel_store()
383+ .read(cx)
384+ .has_new_messages(channel_id)
385+ .unwrap()
386+ });
387+
388+ assert!(b_has_messages);
389+
390+ // Closing the chat should re-enable change tracking
391+ cx_b.update(|_| drop(chat_panel_b));
392+
393+ channel_chat_a
394+ .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
395+ .await
396+ .unwrap();
397+
398+ executor.run_until_parked();
399+
400+ let b_has_messages = cx_b.update(|cx| {
401+ client_b
402+ .channel_store()
403+ .read(cx)
404+ .has_new_messages(channel_id)
405+ .unwrap()
406+ });
407+
408+ assert!(b_has_messages);
409+}
crates/collab/src/tests/following_tests.rs+223−6
@@ -1,9 +1,12 @@
11 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2−use call::ActiveCall;
3−use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
2+use call::{ActiveCall, ParticipantLocation};
3+use collab_ui::{
4+ channel_view::ChannelView,
5+ notifications::project_shared_notification::ProjectSharedNotification,
6+};
47 use editor::{Editor, ExcerptRange, MultiBuffer};
58 use gpui::{
6− point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext,
9+ point, BackgroundExecutor, Context, Entity, SharedString, TestAppContext, View, VisualContext,
710 VisualTestContext,
811 };
912 use language::Capability;
@@ -1568,6 +1571,59 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut · 1558 unmodified lines
15681571 });
15691572 }
15701573
1574+#[gpui::test]
1575+async 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+
15711627 #[gpui::test]
15721628 async fn test_following_into_excluded_file(
15731629 mut cx_a: &mut TestAppContext,
@@ -1593,9 +1649,6 @@ async fn test_following_into_excluded_file( · 19 unmodified lines
15931649 let active_call_b = cx_b.read(ActiveCall::global);
15941650 let peer_id_a = client_a.peer_id().unwrap();
15951651
1596− cx_a.update(editor::init);
1597− cx_b.update(editor::init);
1598−
15991652 client_a
16001653 .fs()
16011654 .insert_tree(
@@ -1772,3 +1825,167 @@ fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Ve · 170 unmodified lines
17721825 .collect()
17731826 })
17741827 }
1828+
1829+#[gpui::test(iterations = 10)]
1830+async 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
113113 }
114114 }
115115
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+
116130 pub async fn reset(&self) {
117131 self.app_state.db.reset();
118132 let epoch = self
@@ -619,14 +633,49 @@ impl TestClient { · 500 unmodified lines
619633 "/a",
620634 json!({
621635 "1.txt": "one\none\none",
622− "2.txt": "two\ntwo\ntwo",
623− "3.txt": "three\nthree\nthree",
636+ "2.js": "function two() { return 2; }",
637+ "3.rs": "mod test",
624638 }),
625639 )
626640 .await;
627641 self.build_local_project("/a", cx).await.0
628642 }
629643
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+
630679 pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
631680 cx.update(|cx| {
632681 Project::local(
@@ -670,6 +719,17 @@ impl TestClient { · 37 unmodified lines
670719 })
671720 }
672721
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+
673733 pub fn active_workspace<'a>(
674734 &'a self,
675735 cx: &'a mut TestAppContext,
crates/collab_ui/src/collab_ui.rs+0−1
@@ -111,7 +111,6 @@ fn notification_window_options( · 110 unmodified lines
111111 let screen_bounds = screen.bounds();
112112 let size: Size<GlobalPixels> = window_size.into();
113113
114− // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
115114 let bounds = gpui::Bounds::<GlobalPixels> {
116115 origin: screen_bounds.upper_right()
117116 - point(
crates/editor/src/display_map/wrap_map.rs+24−30
@@ -11,7 +11,6 @@ use smol::future::yield_now; · 10 unmodified lines
1111 use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
1212 use sum_tree::{Bias, Cursor, SumTree};
1313 use text::Patch;
14−use util::ResultExt;
1514
1615 pub use super::tab_map::TextSummary;
1716 pub type WrapEdit = text::Edit<u32>;
@@ -154,26 +153,24 @@ impl WrapMap { · 136 unmodified lines
154153
155154 if let Some(wrap_width) = self.wrap_width {
156155 let mut new_snapshot = self.snapshot.clone();
157− let mut edits = Patch::default();
156+
158157 let text_system = cx.text_system().clone();
159158 let (font, font_size) = self.font_with_size.clone();
160159 let task = cx.background_executor().spawn(async move {
161− if let Some(mut line_wrapper) = text_system.line_wrapper(font, font_size).log_err()
162− {
163− let tab_snapshot = new_snapshot.tab_snapshot.clone();
164− let range = TabPoint::zero()..tab_snapshot.max_point();
165− edits = new_snapshot
166− .update(
167− tab_snapshot,
168− &[TabEdit {
169− old: range.clone(),
170− new: range.clone(),
171− }],
172− wrap_width,
173− &mut line_wrapper,
174− )
175− .await;
176− }
160+ let mut line_wrapper = text_system.line_wrapper(font, font_size);
161+ let tab_snapshot = new_snapshot.tab_snapshot.clone();
162+ let range = TabPoint::zero()..tab_snapshot.max_point();
163+ let edits = new_snapshot
164+ .update(
165+ tab_snapshot,
166+ &[TabEdit {
167+ old: range.clone(),
168+ new: range.clone(),
169+ }],
170+ wrap_width,
171+ &mut line_wrapper,
172+ )
173+ .await;
177174 (new_snapshot, edits)
178175 });
179176
@@ -245,15 +242,12 @@ impl WrapMap { · 65 unmodified lines
245242 let (font, font_size) = self.font_with_size.clone();
246243 let update_task = cx.background_executor().spawn(async move {
247244 let mut edits = Patch::default();
248− if let Some(mut line_wrapper) =
249− text_system.line_wrapper(font, font_size).log_err()
250− {
251− for (tab_snapshot, tab_edits) in pending_edits {
252− let wrap_edits = snapshot
253− .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
254− .await;
255− edits = edits.compose(&wrap_edits);
256− }
245+ let mut line_wrapper = text_system.line_wrapper(font, font_size);
246+ for (tab_snapshot, tab_edits) in pending_edits {
247+ let wrap_edits = snapshot
248+ .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
249+ .await;
250+ edits = edits.compose(&wrap_edits);
257251 }
258252 (snapshot, edits)
259253 });
@@ -1043,7 +1037,7 @@ mod tests { · 783 unmodified lines
10431037
10441038 #[gpui::test(iterations = 100)]
10451039 async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
1046− // todo!() this test is flaky
1040+ // todo this test is flaky
10471041 init_test(cx);
10481042
10491043 cx.background_executor.set_block_on_ticks(0..=50);
@@ -1059,7 +1053,7 @@ mod tests { · 9 unmodified lines
10591053 };
10601054 let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
10611055 let font = font("Helvetica");
1062− let _font_id = text_system.font_id(&font).unwrap();
1056+ let _font_id = text_system.font_id(&font);
10631057 let font_size = px(14.0);
10641058
10651059 log::info!("Tab size: {}", tab_size);
@@ -1086,7 +1080,7 @@ mod tests { · 20 unmodified lines
10861080 let tabs_snapshot = tab_map.set_max_expansion_column(32);
10871081 log::info!("TabMap text: {:?}", tabs_snapshot.text());
10881082
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);
10901084 let unwrapped_text = tabs_snapshot.text();
10911085 let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
10921086
crates/editor/src/editor.rs+4−2
@@ -7677,7 +7677,6 @@ impl Editor { · 7676 unmodified lines
76777677 scrollbar_width: cx.editor_style.scrollbar_width,
76787678 syntax: cx.editor_style.syntax.clone(),
76797679 status: cx.editor_style.status.clone(),
7680− // todo!("what about the rest of the highlight style parts for inlays and suggestions?")
76817680 inlays_style: HighlightStyle {
76827681 color: Some(cx.theme().status().hint),
76837682 font_weight: Some(FontWeight::BOLD),
@@ -8678,6 +8677,10 @@ impl Editor { · 994 unmodified lines
86788677 }
86798678 }
86808679 }
8680+
8681+ let Some(project) = &self.project else { return };
8682+ let telemetry = project.read(cx).client().telemetry().clone();
8683+ telemetry.log_edit_event("editor");
86818684 }
86828685 multi_buffer::Event::ExcerptsAdded {
86838686 buffer,
@@ -9346,7 +9349,6 @@ impl Render for Editor { · 662 unmodified lines
93469349 scrollbar_width: px(12.),
93479350 syntax: cx.theme().syntax().clone(),
93489351 status: cx.theme().status().clone(),
9349− // todo!("what about the rest of the highlight style parts?")
93509352 inlays_style: HighlightStyle {
93519353 color: Some(cx.theme().status().hint),
93529354 font_weight: Some(FontWeight::BOLD),
crates/editor/src/element.rs+38−38
@@ -809,13 +809,18 @@ impl EditorElement { · 808 unmodified lines
809809 // the hunk might include the rows of that header.
810810 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
811811 // 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.
813813 let end_row_in_current_excerpt = layout
814814 .position_map
815815 .snapshot
816816 .blocks_in_range(start_row..end_row)
817− .next()
818− .map(|(start_row, _)| start_row)
817+ .find_map(|(start_row, block)| {
818+ if matches!(block, TransformBlock::ExcerptHeader { .. }) {
819+ Some(start_row)
820+ } else {
821+ None
822+ }
823+ })
819824 .unwrap_or(end_row);
820825
821826 let start_y = start_row as f32 * line_height - scroll_top;
@@ -878,16 +883,23 @@ impl EditorElement { · 56 unmodified lines
878883 let fold_corner_radius = 0.15 * layout.position_map.line_height;
879884 cx.with_element_id(Some("folds"), |cx| {
880885 let snapshot = &layout.position_map.snapshot;
886+
881887 for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) {
882888 let fold_range = fold.range.clone();
883889 let display_range = fold.range.start.to_display_point(&snapshot)
884890 ..fold.range.end.to_display_point(&snapshot);
885891 debug_assert_eq!(display_range.start.row(), display_range.end.row());
886892 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+ };
887902
888− let line_layout = &layout.position_map.line_layouts
889− [(row - layout.visible_display_row_range.start) as usize]
890− .line;
891903 let start_x = content_origin.x
892904 + line_layout.x_for_index(display_range.start.column() as usize)
893905 - layout.position_map.scroll_position.x;
@@ -1010,7 +1022,6 @@ impl EditorElement { · 116 unmodified lines
10101022 .chars_at(cursor_position)
10111023 .next()
10121024 .and_then(|(character, _)| {
1013− // todo!() currently shape_line panics if text conatins newlines
10141025 let text = if character == '\n' {
10151026 SharedString::from(" ")
10161027 } else {
@@ -2258,11 +2269,9 @@ impl EditorElement { · 1241 unmodified lines
22582269 .map_or(range.context.start, |primary| primary.start);
22592270 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
22602271
2261− let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| {
2272+ cx.listener_for(&self.editor, move |editor, _, cx| {
22622273 editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
2263− });
2264−
2265− jump_handler
2274+ })
22662275 });
22672276
22682277 let element = if *starts_new_buffer {
@@ -2342,34 +2351,25 @@ impl EditorElement { · 73 unmodified lines
23422351 .text_color(cx.theme().colors().editor_line_number)
23432352 .child("..."),
23442353 )
2345− .map(|this| {
2346− if let Some(jump_handler) = jump_handler {
2347− this.child(
2348− ButtonLike::new("jump to collapsed context")
2349− .style(ButtonStyle::Transparent)
2350− .full_width()
2351− .on_click(jump_handler)
2352− .tooltip(|cx| {
2353− Tooltip::for_action(
2354− "Jump to Buffer",
2355− &OpenExcerpts,
2356− cx,
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− ),
2354+ .child(
2355+ ButtonLike::new("jump to collapsed context")
2356+ .style(ButtonStyle::Transparent)
2357+ .full_width()
2358+ .child(
2359+ div()
2360+ .h_px()
2361+ .w_full()
2362+ .bg(cx.theme().colors().border_variant)
2363+ .group_hover("", |style| {
2364+ style.bg(cx.theme().colors().border)
2365+ }),
23682366 )
2369− } else {
2370− this.child(div().size_full().bg(gpui::green()))
2371− }
2372− })
2367+ .when_some(jump_handler, |this, jump_handler| {
2368+ this.on_click(jump_handler).tooltip(|cx| {
2369+ Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
2370+ })
2371+ }),
2372+ )
23732373 };
23742374 element.into_any()
23752375 }
crates/editor/src/hover_popover.rs+4−6
@@ -2,13 +2,13 @@ use crate::{ · 1 unmodified line
22 display_map::{InlayOffset, ToDisplayPoint},
33 link_go_to_definition::{InlayHighlight, RangeInEditor},
44 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
5− ExcerptId, RangeToAnchorExt,
5+ ExcerptId, Hover, RangeToAnchorExt,
66 };
77 use futures::FutureExt;
88 use gpui::{
9− actions, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model,
10− MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
11− Task, ViewContext, WeakView,
9+ div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, MouseButton,
10+ ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
11+ ViewContext, WeakView,
1212 };
1313 use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
1414
@@ -27,8 +27,6 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; · 12 unmodified lines
2727 pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
2828 pub const HOVER_POPOVER_GAP: Pixels = px(10.);
2929
30−actions!(editor, [Hover]);
31−
3230 /// Bindable action which uses the most recent selection head to trigger a hover
3331 pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
3432 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
8282
8383 let pane = pane.downgrade();
8484 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")?;
8688 let editor = pane.update(&mut cx, |pane, cx| {
8789 let mut editors = pane.items_of_type::<Self>();
8890 editors.find(|editor| {
crates/feedback/src/feedback_modal.rs+1−40
@@ -525,43 +525,4 @@ impl Render for FeedbackModal { · 524 unmodified lines
525525 }
526526 }
527527
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−// }
528+// TODO: Testing of various button states, dismissal prompts, etc. :)
crates/file_finder/Cargo.toml+1−0
@@ -23,6 +23,7 @@ theme = { path = "../theme" } · 22 unmodified lines
2323 ui = { path = "../ui" }
2424 workspace = { path = "../workspace" }
2525 postage.workspace = true
26+anyhow.workspace = true
2627 serde.workspace = true
2728
2829 [dev-dependencies]
crates/file_finder/src/file_finder.rs+125−2
@@ -312,7 +312,7 @@ impl FileFinderDelegate { · 311 unmodified lines
312312 cx: &mut ViewContext<FileFinder>,
313313 ) -> Self {
314314 cx.observe(&project, |file_finder, _, cx| {
315− //todo!() We should probably not re-render on every project anything
315+ //todo We should probably not re-render on every project anything
316316 file_finder
317317 .picker
318318 .update(cx, |picker, cx| picker.refresh(cx))
@@ -519,6 +519,62 @@ impl FileFinderDelegate { · 200 unmodified lines
519519
520520 (file_name, file_name_positions, full_path, path_positions)
521521 }
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+ }
522578 }
523579
524580 impl PickerDelegate for FileFinderDelegate {
@@ -588,7 +644,12 @@ impl PickerDelegate for FileFinderDelegate { · 63 unmodified lines
588644 })
589645 })
590646 .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+ }
592653 }
593654 }
594655
@@ -818,6 +879,68 @@ mod tests { · 223 unmodified lines
818879 }
819880 }
820881
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+
821944 #[gpui::test]
822945 async fn test_complex_path(cx: &mut TestAppContext) {
823946 let app_state = init_test(cx);
crates/gpui/src/action.rs+1−1
@@ -170,7 +170,7 @@ impl ActionRegistry { · 169 unmodified lines
170170 macro_rules! actions {
171171 ($namespace:path, [ $($name:ident),* $(,)? ]) => {
172172 $(
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)]
174174 #[serde(crate = "gpui::private::serde")]
175175 pub struct $name;
176176
crates/gpui/src/keymap/matcher.rs+375−370
@@ -28,11 +28,11 @@ impl KeystrokeMatcher { · 27 unmodified lines
2828
2929 /// Pushes a keystroke onto the matcher.
3030 /// The result of the new keystroke is returned:
31− /// KeyMatch::None =>
31+ /// - KeyMatch::None =>
3232 /// No match is valid for this key given any pending keystrokes.
33− /// KeyMatch::Pending =>
33+ /// - KeyMatch::Pending =>
3434 /// There exist bindings which are still waiting for more keys.
35− /// KeyMatch::Complete(matches) =>
35+ /// - KeyMatch::Complete(matches) =>
3636 /// One or more bindings have received the necessary key presses.
3737 /// Bindings added later will take precedence over earlier bindings.
3838 pub fn match_keystroke(
@@ -77,12 +77,10 @@ impl KeystrokeMatcher { · 38 unmodified lines
7777
7878 if let Some(pending_key) = pending_key {
7979 self.pending_keystrokes.push(pending_key);
80− }
81−
82− if self.pending_keystrokes.is_empty() {
83− KeyMatch::None
84− } else {
8580 KeyMatch::Pending
81+ } else {
82+ self.pending_keystrokes.clear();
83+ KeyMatch::None
8684 }
8785 }
8886 }
@@ -98,367 +96,374 @@ impl KeyMatch { · 9 unmodified lines
9896 pub fn is_some(&self) -> bool {
9997 matches!(self, KeyMatch::Some(_))
10098 }
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+
108+impl 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+ }
101129 }
102130
103−// #[cfg(test)]
104−// mod tests {
105−// use anyhow::Result;
106−// use serde::Deserialize;
107−
108−// use crate::{actions, impl_actions, keymap_matcher::ActionContext};
109−
110−// use super::*;
111−
112−// #[test]
113−// fn test_keymap_and_view_ordering() -> Result<()> {
114−// actions!(test, [EditorAction, ProjectPanelAction]);
115−
116−// let mut editor = ActionContext::default();
117−// editor.add_identifier("Editor");
118−
119−// let mut project_panel = ActionContext::default();
120−// project_panel.add_identifier("ProjectPanel");
121−
122−// // Editor 'deeper' in than project panel
123−// let dispatch_path = vec![(2, editor), (1, project_panel)];
124−
125−// // But editor actions 'higher' up in keymap
126−// let keymap = Keymap::new(vec![
127−// Binding::new("left", EditorAction, Some("Editor")),
128−// Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
129−// ]);
130−
131−// let mut matcher = KeymapMatcher::new(keymap);
132−
133−// assert_eq!(
134−// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
135−// KeyMatch::Matches(vec![
136−// (2, Box::new(EditorAction)),
137−// (1, Box::new(ProjectPanelAction)),
138−// ]),
139−// );
140−
141−// Ok(())
142−// }
143−
144−// #[test]
145−// fn test_push_keystroke() -> Result<()> {
146−// actions!(test, [B, AB, C, D, DA, E, EF]);
147−
148−// let mut context1 = ActionContext::default();
149−// context1.add_identifier("1");
150−
151−// let mut context2 = ActionContext::default();
152−// context2.add_identifier("2");
153−
154−// let dispatch_path = vec![(2, context2), (1, context1)];
155−
156−// let keymap = Keymap::new(vec![
157−// Binding::new("a b", AB, Some("1")),
158−// Binding::new("b", B, Some("2")),
159−// Binding::new("c", C, Some("2")),
160−// Binding::new("d", D, Some("1")),
161−// Binding::new("d", D, Some("2")),
162−// Binding::new("d a", DA, Some("2")),
163−// ]);
164−
165−// let mut matcher = KeymapMatcher::new(keymap);
166−
167−// // Binding with pending prefix always takes precedence
168−// assert_eq!(
169−// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
170−// KeyMatch::Pending,
171−// );
172−// // B alone doesn't match because a was pending, so AB is returned instead
173−// assert_eq!(
174−// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
175−// KeyMatch::Matches(vec![(1, Box::new(AB))]),
176−// );
177−// assert!(!matcher.has_pending_keystrokes());
178−
179−// // Without an a prefix, B is dispatched like expected
180−// assert_eq!(
181−// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
182−// KeyMatch::Matches(vec![(2, Box::new(B))]),
183−// );
184−// assert!(!matcher.has_pending_keystrokes());
185−
186−// // If a is prefixed, C will not be dispatched because there
187−// // was a pending binding for it
188−// assert_eq!(
189−// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
190−// KeyMatch::Pending,
191−// );
192−// assert_eq!(
193−// matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
194−// KeyMatch::None,
195−// );
196−// assert!(!matcher.has_pending_keystrokes());
197−
198−// // If a single keystroke matches multiple bindings in the tree
199−// // all of them are returned so that we can fallback if the action
200−// // handler decides to propagate the action
201−// assert_eq!(
202−// matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
203−// KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
204−// );
205−
206−// // If none of the d action handlers consume the binding, a pending
207−// // binding may then be used
208−// assert_eq!(
209−// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
210−// KeyMatch::Matches(vec![(2, Box::new(DA))]),
211−// );
212−// assert!(!matcher.has_pending_keystrokes());
213−
214−// Ok(())
215−// }
216−
217−// #[test]
218−// fn test_keystroke_parsing() -> Result<()> {
219−// assert_eq!(
220−// Keystroke::parse("ctrl-p")?,
221−// Keystroke {
222−// key: "p".into(),
223−// ctrl: true,
224−// alt: false,
225−// shift: false,
226−// cmd: false,
227−// function: false,
228−// ime_key: None,
229−// }
230−// );
231−
232−// assert_eq!(
233−// Keystroke::parse("alt-shift-down")?,
234−// Keystroke {
235−// key: "down".into(),
236−// ctrl: false,
237−// alt: true,
238−// shift: true,
239−// cmd: false,
240−// function: false,
241−// ime_key: None,
242−// }
243−// );
244−
245−// assert_eq!(
246−// Keystroke::parse("shift-cmd--")?,
247−// Keystroke {
248−// key: "-".into(),
249−// ctrl: false,
250−// alt: false,
251−// shift: true,
252−// cmd: true,
253−// function: false,
254−// ime_key: None,
255−// }
256−// );
257−
258−// Ok(())
259−// }
260−
261−// #[test]
262−// fn test_context_predicate_parsing() -> Result<()> {
263−// use KeymapContextPredicate::*;
264−
265−// assert_eq!(
266−// KeymapContextPredicate::parse("a && (b == c || d != e)")?,
267−// And(
268−// Box::new(Identifier("a".into())),
269−// Box::new(Or(
270−// Box::new(Equal("b".into(), "c".into())),
271−// Box::new(NotEqual("d".into(), "e".into())),
272−// ))
273−// )
274−// );
275−
276−// assert_eq!(
277−// KeymapContextPredicate::parse("!a")?,
278−// Not(Box::new(Identifier("a".into())),)
279−// );
280−
281−// Ok(())
282−// }
283−
284−// #[test]
285−// fn test_context_predicate_eval() {
286−// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
287−
288−// let mut context = ActionContext::default();
289−// context.add_identifier("a");
290−// assert!(!predicate.eval(&[context]));
291−
292−// let mut context = ActionContext::default();
293−// context.add_identifier("a");
294−// context.add_identifier("b");
295−// assert!(predicate.eval(&[context]));
296−
297−// let mut context = ActionContext::default();
298−// context.add_identifier("a");
299−// context.add_key("c", "x");
300−// assert!(!predicate.eval(&[context]));
301−
302−// let mut context = ActionContext::default();
303−// context.add_identifier("a");
304−// context.add_key("c", "d");
305−// assert!(predicate.eval(&[context]));
306−
307−// let predicate = KeymapContextPredicate::parse("!a").unwrap();
308−// assert!(predicate.eval(&[ActionContext::default()]));
309−// }
310−
311−// #[test]
312−// fn test_context_child_predicate_eval() {
313−// let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
314−// let contexts = [
315−// context_set(&["e", "f"]),
316−// context_set(&["c", "d"]), // match this context
317−// context_set(&["a", "b"]),
318−// ];
319−
320−// assert!(!predicate.eval(&contexts[0..]));
321−// assert!(predicate.eval(&contexts[1..]));
322−// assert!(!predicate.eval(&contexts[2..]));
323−
324−// let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
325−// let contexts = [
326−// context_set(&["f"]),
327−// context_set(&["e"]), // only match this context
328−// context_set(&["c"]),
329−// context_set(&["a", "b"]),
330−// context_set(&["e"]),
331−// context_set(&["c", "d"]),
332−// context_set(&["a", "b"]),
333−// ];
334−
335−// assert!(!predicate.eval(&contexts[0..]));
336−// assert!(predicate.eval(&contexts[1..]));
337−// assert!(!predicate.eval(&contexts[2..]));
338−// assert!(!predicate.eval(&contexts[3..]));
339−// assert!(!predicate.eval(&contexts[4..]));
340−// assert!(!predicate.eval(&contexts[5..]));
341−// assert!(!predicate.eval(&contexts[6..]));
342−
343−// fn context_set(names: &[&str]) -> ActionContext {
344−// let mut keymap = ActionContext::new();
345−// names
346−// .iter()
347−// .for_each(|name| keymap.add_identifier(name.to_string()));
348−// keymap
349−// }
350−// }
351−
352−// #[test]
353−// fn test_matcher() -> Result<()> {
354−// #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
355−// pub struct A(pub String);
356−// impl_actions!(test, [A]);
357−// actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
358−
359−// #[derive(Clone, Debug, Eq, PartialEq)]
360−// struct ActionArg {
361−// a: &'static str,
362−// }
363−
364−// let keymap = Keymap::new(vec![
365−// Binding::new("a", A("x".to_string()), Some("a")),
366−// Binding::new("b", B, Some("a")),
367−// Binding::new("a b", Ab, Some("a || b")),
368−// Binding::new("$", Dollar, Some("a")),
369−// Binding::new("\"", Quote, Some("a")),
370−// Binding::new("alt-s", Ess, Some("a")),
371−// Binding::new("ctrl-`", Backtick, Some("a")),
372−// ]);
373−
374−// let mut context_a = ActionContext::default();
375−// context_a.add_identifier("a");
376−
377−// let mut context_b = ActionContext::default();
378−// context_b.add_identifier("b");
379−
380−// let mut matcher = KeymapMatcher::new(keymap);
381−
382−// // Basic match
383−// assert_eq!(
384−// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
385−// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
386−// );
387−// matcher.clear_pending();
388−
389−// // Multi-keystroke match
390−// assert_eq!(
391−// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
392−// KeyMatch::Pending
393−// );
394−// assert_eq!(
395−// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
396−// KeyMatch::Matches(vec![(1, Box::new(Ab))])
397−// );
398−// matcher.clear_pending();
399−
400−// // Failed matches don't interfere with matching subsequent keys
401−// assert_eq!(
402−// matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
403−// KeyMatch::None
404−// );
405−// assert_eq!(
406−// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
407−// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
408−// );
409−// matcher.clear_pending();
410−
411−// // Pending keystrokes are cleared when the context changes
412−// assert_eq!(
413−// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
414−// KeyMatch::Pending
415−// );
416−// assert_eq!(
417−// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
418−// KeyMatch::None
419−// );
420−// matcher.clear_pending();
421−
422−// let mut context_c = ActionContext::default();
423−// context_c.add_identifier("c");
424−
425−// // Pending keystrokes are maintained per-view
426−// assert_eq!(
427−// matcher.match_keystroke(
428−// Keystroke::parse("a")?,
429−// vec![(1, context_b.clone()), (2, context_c.clone())]
430−// ),
431−// KeyMatch::Pending
432−// );
433−// assert_eq!(
434−// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
435−// KeyMatch::Matches(vec![(1, Box::new(Ab))])
436−// );
437−
438−// // handle Czech $ (option + 4 key)
439−// assert_eq!(
440−// matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
441−// KeyMatch::Matches(vec![(1, Box::new(Dollar))])
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−// }
131+#[cfg(test)]
132+mod tests {
133+
134+ use serde_derive::Deserialize;
135+
136+ use super::*;
137+ use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
138+ use crate::{actions, KeyBinding};
139+
140+ #[test]
141+ fn test_keymap_and_view_ordering() {
142+ actions!(test, [EditorAction, ProjectPanelAction]);
143+
144+ let mut editor = KeyContext::default();
145+ editor.add("Editor");
146+
147+ let mut project_panel = KeyContext::default();
148+ project_panel.add("ProjectPanel");
149+
150+ // Editor 'deeper' in than project panel
151+ let dispatch_path = vec![project_panel, editor];
152+
153+ // But editor actions 'higher' up in keymap
154+ let keymap = Keymap::new(vec![
155+ KeyBinding::new("left", EditorAction, Some("Editor")),
156+ KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
157+ ]);
158+
159+ let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
160+
161+ let matches = matcher
162+ .match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
163+ .matches()
164+ .unwrap();
165+
166+ assert!(matches[0].partial_eq(&EditorAction));
167+ assert!(matches.get(1).is_none());
168+ }
169+
170+ #[test]
171+ fn test_multi_keystroke_match() {
172+ actions!(test, [B, AB, C, D, DA, E, EF]);
173+
174+ let mut context1 = KeyContext::default();
175+ context1.add("1");
176+
177+ let mut context2 = KeyContext::default();
178+ context2.add("2");
179+
180+ let dispatch_path = vec![context2, context1];
181+
182+ let keymap = Keymap::new(vec![
183+ KeyBinding::new("a b", AB, Some("1")),
184+ KeyBinding::new("b", B, Some("2")),
185+ KeyBinding::new("c", C, Some("2")),
186+ KeyBinding::new("d", D, Some("1")),
187+ KeyBinding::new("d", D, Some("2")),
188+ KeyBinding::new("d a", DA, Some("2")),
189+ ]);
190+
191+ let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
192+
193+ // Binding with pending prefix always takes precedence
194+ assert_eq!(
195+ matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
196+ KeyMatch::Pending,
197+ );
198+ // B alone doesn't match because a was pending, so AB is returned instead
199+ assert_eq!(
200+ matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path),
201+ KeyMatch::Some(vec![Box::new(AB)]),
202+ );
203+ assert!(!matcher.has_pending_keystrokes());
204+
205+ // Without an a prefix, B is dispatched like expected
206+ assert_eq!(
207+ matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]),
208+ KeyMatch::Some(vec![Box::new(B)]),
209+ );
210+ assert!(!matcher.has_pending_keystrokes());
211+
212+ eprintln!("PROBLEM AREA");
213+ // If a is prefixed, C will not be dispatched because there
214+ // was a pending binding for it
215+ assert_eq!(
216+ matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
217+ KeyMatch::Pending,
218+ );
219+ assert_eq!(
220+ matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path),
221+ KeyMatch::None,
222+ );
223+ assert!(!matcher.has_pending_keystrokes());
224+
225+ // If a single keystroke matches multiple bindings in the tree
226+ // only one of them is returned.
227+ assert_eq!(
228+ matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path),
229+ KeyMatch::Some(vec![Box::new(D)]),
230+ );
231+ }
232+
233+ #[test]
234+ fn test_keystroke_parsing() {
235+ assert_eq!(
236+ Keystroke::parse("ctrl-p").unwrap(),
237+ Keystroke {
238+ key: "p".into(),
239+ modifiers: Modifiers {
240+ control: true,
241+ alt: false,
242+ shift: false,
243+ command: false,
244+ function: false,
245+ },
246+ ime_key: None,
247+ }
248+ );
249+
250+ assert_eq!(
251+ Keystroke::parse("alt-shift-down").unwrap(),
252+ Keystroke {
253+ key: "down".into(),
254+ modifiers: Modifiers {
255+ control: false,
256+ alt: true,
257+ shift: true,
258+ command: false,
259+ function: false,
260+ },
261+ ime_key: None,
262+ }
263+ );
264+
265+ assert_eq!(
266+ Keystroke::parse("shift-cmd--").unwrap(),
267+ Keystroke {
268+ key: "-".into(),
269+ modifiers: Modifiers {
270+ control: false,
271+ alt: false,
272+ shift: true,
273+ command: true,
274+ function: false,
275+ },
276+ ime_key: None,
277+ }
278+ );
279+ }
280+
281+ #[test]
282+ fn test_context_predicate_parsing() {
283+ use KeyBindingContextPredicate::*;
284+
285+ assert_eq!(
286+ KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
287+ And(
288+ Box::new(Identifier("a".into())),
289+ Box::new(Or(
290+ Box::new(Equal("b".into(), "c".into())),
291+ Box::new(NotEqual("d".into(), "e".into())),
292+ ))
293+ )
294+ );
295+
296+ assert_eq!(
297+ KeyBindingContextPredicate::parse("!a").unwrap(),
298+ Not(Box::new(Identifier("a".into())),)
299+ );
300+ }
301+
302+ #[test]
303+ fn test_context_predicate_eval() {
304+ let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
305+
306+ let mut context = KeyContext::default();
307+ context.add("a");
308+ assert!(!predicate.eval(&[context]));
309+
310+ let mut context = KeyContext::default();
311+ context.add("a");
312+ context.add("b");
313+ assert!(predicate.eval(&[context]));
314+
315+ let mut context = KeyContext::default();
316+ context.add("a");
317+ context.set("c", "x");
318+ assert!(!predicate.eval(&[context]));
319+
320+ let mut context = KeyContext::default();
321+ context.add("a");
322+ context.set("c", "d");
323+ assert!(predicate.eval(&[context]));
324+
325+ let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
326+ assert!(predicate.eval(&[KeyContext::default()]));
327+ }
328+
329+ #[test]
330+ fn test_context_child_predicate_eval() {
331+ let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap();
332+ let contexts = [
333+ context_set(&["a", "b"]),
334+ context_set(&["c", "d"]), // match this context
335+ context_set(&["e", "f"]),
336+ ];
337+
338+ assert!(!predicate.eval(&contexts[..=0]));
339+ assert!(predicate.eval(&contexts[..=1]));
340+ assert!(!predicate.eval(&contexts[..=2]));
341+
342+ let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap();
343+ let contexts = [
344+ context_set(&["a", "b"]),
345+ context_set(&["c", "d"]),
346+ context_set(&["e"]),
347+ context_set(&["a", "b"]),
348+ context_set(&["c"]),
349+ context_set(&["e"]), // only match this context
350+ context_set(&["f"]),
351+ ];
352+
353+ assert!(!predicate.eval(&contexts[..=0]));
354+ assert!(!predicate.eval(&contexts[..=1]));
355+ assert!(!predicate.eval(&contexts[..=2]));
356+ assert!(!predicate.eval(&contexts[..=3]));
357+ assert!(!predicate.eval(&contexts[..=4]));
358+ assert!(predicate.eval(&contexts[..=5]));
359+ assert!(!predicate.eval(&contexts[..=6]));
360+
361+ fn context_set(names: &[&str]) -> KeyContext {
362+ let mut keymap = KeyContext::default();
363+ names.iter().for_each(|name| keymap.add(name.to_string()));
364+ keymap
365+ }
366+ }
367+
368+ #[test]
369+ fn test_matcher() {
370+ #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
371+ pub struct A(pub String);
372+ impl_actions!(test, [A]);
373+ actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
374+
375+ #[derive(Clone, Debug, Eq, PartialEq)]
376+ struct ActionArg {
377+ a: &'static str,
378+ }
379+
380+ let keymap = Keymap::new(vec![
381+ KeyBinding::new("a", A("x".to_string()), Some("a")),
382+ KeyBinding::new("b", B, Some("a")),
383+ KeyBinding::new("a b", Ab, Some("a || b")),
384+ KeyBinding::new("$", Dollar, Some("a")),
385+ KeyBinding::new("\"", Quote, Some("a")),
386+ KeyBinding::new("alt-s", Ess, Some("a")),
387+ KeyBinding::new("ctrl-`", Backtick, Some("a")),
388+ ]);
389+
390+ let mut context_a = KeyContext::default();
391+ context_a.add("a");
392+
393+ let mut context_b = KeyContext::default();
394+ context_b.add("b");
395+
396+ let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
397+
398+ // Basic match
399+ assert_eq!(
400+ matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
401+ KeyMatch::Some(vec![Box::new(A("x".to_string()))])
402+ );
403+ matcher.clear_pending();
404+
405+ // Multi-keystroke match
406+ assert_eq!(
407+ matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]),
408+ KeyMatch::Pending
409+ );
410+ assert_eq!(
411+ matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
412+ KeyMatch::Some(vec![Box::new(Ab)])
413+ );
414+ matcher.clear_pending();
415+
416+ // Failed matches don't interfere with matching subsequent keys
417+ assert_eq!(
418+ matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]),
419+ KeyMatch::None
420+ );
421+ assert_eq!(
422+ matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
423+ KeyMatch::Some(vec![Box::new(A("x".to_string()))])
424+ );
425+ matcher.clear_pending();
426+
427+ let mut context_c = KeyContext::default();
428+ context_c.add("c");
429+
430+ assert_eq!(
431+ matcher.match_keystroke(
432+ &Keystroke::parse("a").unwrap(),
433+ &[context_c.clone(), context_b.clone()]
434+ ),
435+ KeyMatch::Pending
436+ );
437+ assert_eq!(
438+ matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
439+ KeyMatch::Some(vec![Box::new(Ab)])
440+ );
441+
442+ // handle Czech $ (option + 4 key)
443+ assert_eq!(
444+ matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]),
445+ KeyMatch::Some(vec![Box::new(Dollar)])
446+ );
447+
448+ // handle Brazillian quote (quote key then space key)
449+ assert_eq!(
450+ matcher.match_keystroke(
451+ &Keystroke::parse("space->\"").unwrap(),
452+ &[context_a.clone()]
453+ ),
454+ KeyMatch::Some(vec![Box::new(Quote)])
455+ );
456+
457+ // handle ctrl+` on a brazillian keyboard
458+ assert_eq!(
459+ matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
460+ KeyMatch::Some(vec![Box::new(Backtick)])
461+ );
462+
463+ // handle alt-s on a US keyboard
464+ assert_eq!(
465+ matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]),
466+ KeyMatch::Some(vec![Box::new(Ess)])
467+ );
468+ }
469+}
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
978978 unsafe {
979979 if let Some(event) = InputEvent::from_native(native_event, None) {
980980 let platform = get_mac_platform(this);
981− if let Some(callback) = platform.0.lock().event.as_mut() {
982− if !callback(event) {
981+ let mut lock = platform.0.lock();
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 {
983987 return;
984988 }
985989 }
@@ -1004,30 +1008,42 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { · 18 unmodified lines
10041008 extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
10051009 if !has_open_windows {
10061010 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);
10081014 callback();
1015+ platform.0.lock().reopen.get_or_insert(callback);
10091016 }
10101017 }
10111018 }
10121019
10131020 extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
10141021 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);
10161025 callback();
1026+ platform.0.lock().become_active.get_or_insert(callback);
10171027 }
10181028 }
10191029
10201030 extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
10211031 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);
10231035 callback();
1036+ platform.0.lock().resign_active.get_or_insert(callback);
10241037 }
10251038 }
10261039
10271040 extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
10281041 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);
10301045 callback();
1046+ platform.0.lock().quit.get_or_insert(callback);
10311047 }
10321048 }
10331049
@@ -1047,22 +1063,27 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { · 13 unmodified lines
10471063 .collect::<Vec<_>>()
10481064 };
10491065 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);
10511069 callback(urls);
1070+ platform.0.lock().open_urls.get_or_insert(callback);
10521071 }
10531072 }
10541073
10551074 extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
10561075 unsafe {
10571076 let platform = get_mac_platform(this);
1058− let mut platform = platform.0.lock();
1059− if let Some(mut callback) = platform.menu_command.take() {
1077+ let mut lock = platform.0.lock();
1078+ if let Some(mut callback) = lock.menu_command.take() {
10601079 let tag: NSInteger = msg_send![item, tag];
10611080 let index = tag as usize;
1062− if let Some(action) = platform.menu_actions.get(index) {
1063− callback(action.as_ref());
1081+ if let Some(action) = lock.menu_actions.get(index) {
1082+ let action = action.boxed_clone();
1083+ drop(lock);
1084+ callback(&*action);
10641085 }
1065− platform.menu_command = Some(callback);
1086+ platform.0.lock().menu_command.get_or_insert(callback);
10661087 }
10671088 }
10681089 }
@@ -1071,14 +1092,20 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { · 2 unmodified lines
10711092 unsafe {
10721093 let mut result = false;
10731094 let platform = get_mac_platform(this);
1074− let mut platform = platform.0.lock();
1075− if let Some(mut callback) = platform.validate_menu_command.take() {
1095+ let mut lock = platform.0.lock();
1096+ if let Some(mut callback) = lock.validate_menu_command.take() {
10761097 let tag: NSInteger = msg_send![item, tag];
10771098 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);
10791102 result = callback(action.as_ref());
10801103 }
1081− platform.validate_menu_command = Some(callback);
1104+ platform
1105+ .0
1106+ .lock()
1107+ .validate_menu_command
1108+ .get_or_insert(callback);
10821109 }
10831110 result
10841111 }
@@ -1087,10 +1114,11 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { · 2 unmodified lines
10871114 extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
10881115 unsafe {
10891116 let platform = get_mac_platform(this);
1090− let mut platform = platform.0.lock();
1091− if let Some(mut callback) = platform.will_open_menu.take() {
1117+ let mut lock = platform.0.lock();
1118+ if let Some(mut callback) = lock.will_open_menu.take() {
1119+ drop(lock);
10921120 callback();
1093− platform.will_open_menu = Some(callback);
1121+ platform.0.lock().will_open_menu.get_or_insert(callback);
10941122 }
10951123 }
10961124 }
crates/gpui/src/platform/mac/text_system.rs+49−166
@@ -190,6 +190,9 @@ impl MacTextSystemState { · 189 unmodified lines
190190 for font in family.fonts() {
191191 let mut font = font.load()?;
192192 open_type::apply_features(&mut font, features);
193+ let Some(_) = font.glyph_for_char('m') else {
194+ continue;
195+ };
193196 let font_id = FontId(self.fonts.len());
194197 font_ids.push(font_id);
195198 let postscript_name = font.postscript_name().unwrap();
@@ -592,169 +595,49 @@ impl From<FontStyle> for FontkitStyle { · 396 unmodified lines
592595 }
593596 }
594597
595−// #[cfg(test)]
596−// mod tests {
597−// use super::*;
598−// use crate::AppContext;
599−// use font_kit::properties::{Style, Weight};
600−// use platform::FontSystem as _;
601−
602−// #[crate::test(self, retries = 5)]
603−// fn test_layout_str(_: &mut AppContext) {
604−// // This is failing intermittently on CI and we don't have time to figure it out
605−// let fonts = FontSystem::new();
606−// let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
607−// let menlo_regular = RunStyle {
608−// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
609−// color: Default::default(),
610−// underline: Default::default(),
611−// };
612−// let menlo_italic = RunStyle {
613−// font_id: fonts
614−// .select_font(&menlo, Properties::new().style(Style::Italic))
615−// .unwrap(),
616−// color: Default::default(),
617−// underline: Default::default(),
618−// };
619−// let menlo_bold = RunStyle {
620−// font_id: fonts
621−// .select_font(&menlo, Properties::new().weight(Weight::BOLD))
622−// .unwrap(),
623−// color: Default::default(),
624−// underline: Default::default(),
625−// };
626−// assert_ne!(menlo_regular, menlo_italic);
627−// assert_ne!(menlo_regular, menlo_bold);
628−// assert_ne!(menlo_italic, menlo_bold);
629−
630−// let line = fonts.layout_line(
631−// "hello world",
632−// 16.0,
633−// &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
634−// );
635−// assert_eq!(line.runs.len(), 3);
636−// assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
637−// assert_eq!(line.runs[0].glyphs.len(), 2);
638−// assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
639−// assert_eq!(line.runs[1].glyphs.len(), 4);
640−// assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
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−// }
598+#[cfg(test)]
599+mod tests {
600+ use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
601+
602+ #[test]
603+ fn test_wrap_line() {
604+ let fonts = MacTextSystem::new();
605+ let font_id = fonts.font_id(&font("Helvetica")).unwrap();
606+
607+ let line = "one two three four five\n";
608+ let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
609+ assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
610+
611+ let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
612+ let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
613+ assert_eq!(
614+ wrap_boundaries,
615+ &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
616+ );
617+ }
618+
619+ #[test]
620+ fn test_layout_line_bom_char() {
621+ let fonts = MacTextSystem::new();
622+ let font_id = fonts.font_id(&font("Helvetica")).unwrap();
623+ let line = "\u{feff}";
624+ let mut style = FontRun {
625+ font_id,
626+ len: line.len(),
627+ };
628+
629+ let layout = fonts.layout_line(line, px(16.), &[style]);
630+ assert_eq!(layout.len, line.len());
631+ assert!(layout.runs.is_empty());
632+
633+ let line = "a\u{feff}b";
634+ style.len = line.len();
635+ let layout = fonts.layout_line(line, px(16.), &[style]);
636+ assert_eq!(layout.len, line.len());
637+ assert_eq!(layout.runs.len(), 1);
638+ assert_eq!(layout.runs[0].glyphs.len(), 2);
639+ assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a
640+ // There's no glyph for \u{feff}
641+ assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b
642+ }
643+}
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
268268 sel!(windowShouldClose:),
269269 window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
270270 );
271+
271272 decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
272273
273274 decl.add_method(
@@ -683,9 +684,6 @@ impl Drop for MacWindow { · 409 unmodified lines
683684 this.executor
684685 .spawn(async move {
685686 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)
689687 window.close();
690688 }
691689 })
@@ -1104,37 +1102,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: · 412 unmodified lines
11041102 .flatten()
11051103 .is_some();
11061104 if !is_composing {
1107− // if the IME has changed the key, we'll first emit an event with the character
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− }
1105+ handled = callback(InputEvent::KeyDown(event));
11381106 }
11391107
11401108 if !handled {
@@ -1574,6 +1542,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS · 433 unmodified lines
15741542 replacement_range,
15751543 text: text.to_string(),
15761544 });
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+ }
15771548 window_state.lock().pending_key_down = Some(pending_key_down);
15781549 }
15791550 }
crates/gpui/src/text_system.rs+10−15
@@ -65,6 +65,9 @@ impl TextSystem { · 64 unmodified lines
6565 }
6666 }
6767
68+ pub fn all_font_families(&self) -> Vec<String> {
69+ self.platform_text_system.all_font_families()
70+ }
6871 pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
6972 self.platform_text_system.add_fonts(fonts)
7073 }
@@ -368,28 +371,20 @@ impl TextSystem { · 297 unmodified lines
368371 self.line_layout_cache.finish_frame(reused_views)
369372 }
370373
371− pub fn line_wrapper(
372− self: &Arc<Self>,
373− font: Font,
374− font_size: Pixels,
375− ) -> Result<LineWrapperHandle> {
374+ pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
376375 let lock = &mut self.wrapper_pool.lock();
377− let font_id = self.font_id(&font)?;
376+ let font_id = self.resolve_font(&font);
378377 let wrappers = lock
379378 .entry(FontIdWithSize { font_id, font_size })
380379 .or_default();
381− let wrapper = wrappers.pop().map(anyhow::Ok).unwrap_or_else(|| {
382− Ok(LineWrapper::new(
383− font_id,
384− font_size,
385− self.platform_text_system.clone(),
386− ))
387− })?;
380+ let wrapper = wrappers.pop().unwrap_or_else(|| {
381+ LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
382+ });
388383
389− Ok(LineWrapperHandle {
384+ LineWrapperHandle {
390385 wrapper: Some(wrapper),
391386 text_system: self.clone(),
392− })
387+ }
393388 }
394389
395390 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
137137 #[cfg(test)]
138138 mod tests {
139139 use super::*;
140− use crate::{font, TestAppContext, TestDispatcher};
140+ use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
141141 use rand::prelude::*;
142142
143143 #[test]
@@ -206,75 +206,70 @@ mod tests { · 62 unmodified lines
206206 });
207207 }
208208
209− // todo!("move this to a test on TextSystem::layout_text")
210− // todo! repeat this test
211− // #[test]
212− // fn test_wrap_shaped_line() {
213− // App::test().run(|cx| {
214− // let text_system = cx.text_system().clone();
209+ // For compatibility with the test macro
210+ use crate as gpui;
215211
216− // let normal = TextRun {
217− // len: 0,
218− // font: font("Helvetica"),
219− // color: Default::default(),
220− // underline: Default::default(),
221− // };
222− // let bold = TextRun {
223− // len: 0,
224− // font: font("Helvetica").bold(),
225− // color: Default::default(),
226− // underline: Default::default(),
227− // };
212+ #[crate::test]
213+ fn test_wrap_shaped_line(cx: &mut TestAppContext) {
214+ cx.update(|cx| {
215+ let text_system = cx.text_system().clone();
216+
217+ let normal = TextRun {
218+ len: 0,
219+ font: font("Helvetica"),
220+ color: Default::default(),
221+ underline: Default::default(),
222+ background_color: None,
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+ };
228231
229− // impl TextRun {
230− // fn with_len(&self, len: usize) -> Self {
231− // let mut this = self.clone();
232− // this.len = len;
233− // this
234− // }
235− // }
232+ impl TextRun {
233+ fn with_len(&self, len: usize) -> Self {
234+ let mut this = self.clone();
235+ this.len = len;
236+ this
237+ }
238+ }
236239
237− // let text = "aa bbb cccc ddddd eeee".into();
238− // let lines = text_system
239− // .layout_text(
240− // &text,
241− // px(16.),
242− // &[
243− // normal.with_len(4),
244− // bold.with_len(5),
245− // normal.with_len(6),
246− // bold.with_len(1),
247− // normal.with_len(7),
248− // ],
249− // None,
250− // )
251− // .unwrap();
252− // let line = &lines[0];
240+ let text = "aa bbb cccc ddddd eeee".into();
241+ let lines = text_system
242+ .shape_text(
243+ text,
244+ px(16.),
245+ &[
246+ normal.with_len(4),
247+ bold.with_len(5),
248+ normal.with_len(6),
249+ bold.with_len(1),
250+ normal.with_len(7),
251+ ],
252+ Some(px(72.)),
253+ )
254+ .unwrap();
253255
254− // let mut wrapper = LineWrapper::new(
255− // text_system.font_id(&normal.font).unwrap(),
256− // px(16.),
257− // text_system.platform_text_system.clone(),
258− // );
259− // assert_eq!(
260− // wrapper
261− // .wrap_shaped_line(&text, &line, px(72.))
262− // .collect::<Vec<_>>(),
263− // &[
264− // ShapedBoundary {
265− // run_ix: 1,
266− // glyph_ix: 3
267− // },
268− // ShapedBoundary {
269− // run_ix: 2,
270− // glyph_ix: 3
271− // },
272− // ShapedBoundary {
273− // run_ix: 4,
274− // glyph_ix: 2
275− // }
276− // ],
277− // );
278− // });
279− // }
256+ assert_eq!(
257+ lines[0].layout.wrap_boundaries(),
258+ &[
259+ WrapBoundary {
260+ run_ix: 1,
261+ glyph_ix: 3
262+ },
263+ WrapBoundary {
264+ run_ix: 2,
265+ glyph_ix: 3
266+ },
267+ WrapBoundary {
268+ run_ix: 4,
269+ glyph_ix: 2
270+ }
271+ ],
272+ );
273+ });
274+ }
280275 }
crates/gpui/src/window.rs+13−4
@@ -1545,9 +1545,6 @@ impl<'a> WindowContext<'a> { · 1544 unmodified lines
15451545 .finish(&mut self.window.rendered_frame);
15461546 ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
15471547
1548− self.window.refreshing = false;
1549− self.window.drawing = false;
1550−
15511548 let previous_focus_path = self.window.rendered_frame.focus_path();
15521549 let previous_window_active = self.window.rendered_frame.window_active;
15531550 mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
@@ -1586,6 +1583,8 @@ impl<'a> WindowContext<'a> { · 32 unmodified lines
15861583 self.window
15871584 .platform_window
15881585 .draw(&self.window.rendered_frame.scene);
1586+ self.window.refreshing = false;
1587+ self.window.drawing = false;
15891588 }
15901589
15911590 /// Dispatch a mouse or keyboard event on the window.
@@ -2158,7 +2157,17 @@ impl<'a> WindowContext<'a> { · 566 unmodified lines
21582157 let mut this = self.to_async();
21592158 self.window
21602159 .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+ }))
21622171 }
21632172 }
21642173
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift+47−3
@@ -12,6 +12,8 @@ class LKRoomDelegate: RoomDelegate { · 11 unmodified lines
1212 var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
1313 var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
1414 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
1517
1618 init(
1719 data: UnsafeRawPointer,
@@ -21,7 +23,10 @@ class LKRoomDelegate: RoomDelegate { · 3 unmodified lines
2123 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
2224 onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
2325 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+ )
2530 {
2631 self.data = data
2732 self.onDidDisconnect = onDidDisconnect
@@ -31,6 +36,8 @@ class LKRoomDelegate: RoomDelegate { · 3 unmodified lines
3136 self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
3237 self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
3338 self.onActiveSpeakersChanged = onActiveSpeakersChanged
39+ self.onDidPublishOrUnpublishLocalAudioTrack = onDidPublishOrUnpublishLocalAudioTrack
40+ self.onDidPublishOrUnpublishLocalVideoTrack = onDidPublishOrUnpublishLocalVideoTrack
3441 }
3542
3643 func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
@@ -65,6 +72,22 @@ class LKRoomDelegate: RoomDelegate { · 28 unmodified lines
6572 self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
6673 }
6774 }
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+ }
6891 }
6992
7093 class LKVideoRenderer: NSObject, VideoRenderer {
@@ -109,7 +132,9 @@ public func LKRoomDelegateCreate( · 38 unmodified lines
109132 onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
110133 onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
111134 onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
112− onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
135+ 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
113138 ) -> UnsafeMutableRawPointer {
114139 let delegate = LKRoomDelegate(
115140 data: data,
@@ -119,7 +144,9 @@ public func LKRoomDelegateCreate( · 3 unmodified lines
119144 onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
120145 onActiveSpeakersChanged: onActiveSpeakerChanged,
121146 onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
122− onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
147+ onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack,
148+ onDidPublishOrUnpublishLocalAudioTrack: onDidPublishOrUnpublishLocalAudioTrack,
149+ onDidPublishOrUnpublishLocalVideoTrack: onDidPublishOrUnpublishLocalVideoTrack
123150 )
124151 return Unmanaged.passRetained(delegate).toOpaque()
125152 }
@@ -292,6 +319,14 @@ public func LKLocalTrackPublicationSetMute( · 166 unmodified lines
292319 }
293320 }
294321
322+@_cdecl("LKLocalTrackPublicationIsMuted")
323+public func LKLocalTrackPublicationIsMuted(
324+ publication: UnsafeRawPointer
325+) -> Bool {
326+ let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
327+ return publication.muted
328+}
329+
295330 @_cdecl("LKRemoteTrackPublicationSetEnabled")
296331 public func LKRemoteTrackPublicationSetEnabled(
297332 publication: UnsafeRawPointer,
@@ -325,3 +360,12 @@ public func LKRemoteTrackPublicationGetSid( · 27 unmodified lines
325360
326361 return publication.sid as CFString
327362 }
363+
364+@_cdecl("LKLocalTrackPublicationGetSid")
365+public 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
22
33 use futures::StreamExt;
44 use gpui::{actions, KeyBinding, Menu, MenuItem};
5−use live_kit_client::{
6− LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
7−};
5+use live_kit_client::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate};
86 use live_kit_server::token::{self, VideoGrant};
97 use log::LevelFilter;
108 use simplelog::SimpleLogger;
@@ -60,12 +58,12 @@ fn main() { · 49 unmodified lines
6058 let room_b = Room::new();
6159 room_b.connect(&live_kit_url, &user2_token).await.unwrap();
6260
63− let mut audio_track_updates = room_b.remote_audio_track_updates();
61+ let mut room_updates = room_b.updates();
6462 let audio_track = LocalAudioTrack::create();
6563 let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap();
6664
67− if let RemoteAudioTrackUpdate::Subscribed(track, _) =
68− audio_track_updates.next().await.unwrap()
65+ if let RoomUpdate::SubscribedToRemoteAudioTrack(track, _) =
66+ room_updates.next().await.unwrap()
6967 {
7068 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
7169 assert_eq!(remote_tracks.len(), 1);
@@ -78,8 +76,8 @@ fn main() { · 6 unmodified lines
7876 audio_track_publication.set_mute(true).await.unwrap();
7977
8078 println!("waiting for mute changed!");
81− if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
82− audio_track_updates.next().await.unwrap()
79+ if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
80+ room_updates.next().await.unwrap()
8381 {
8482 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
8583 assert_eq!(remote_tracks[0].sid(), track_id);
@@ -90,8 +88,8 @@ fn main() { · 4 unmodified lines
9088
9189 audio_track_publication.set_mute(false).await.unwrap();
9290
93− if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
94− audio_track_updates.next().await.unwrap()
91+ if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
92+ room_updates.next().await.unwrap()
9593 {
9694 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
9795 assert_eq!(remote_tracks[0].sid(), track_id);
@@ -110,13 +108,13 @@ fn main() { · 12 unmodified lines
110108 room_a.unpublish_track(audio_track_publication);
111109
112110 // Clear out any active speakers changed messages
113− let mut next = audio_track_updates.next().await.unwrap();
114− while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next {
111+ let mut next = room_updates.next().await.unwrap();
112+ while let RoomUpdate::ActiveSpeakersChanged { speakers } = next {
115113 println!("Speakers changed: {:?}", speakers);
116− next = audio_track_updates.next().await.unwrap();
114+ next = room_updates.next().await.unwrap();
117115 }
118116
119− if let RemoteAudioTrackUpdate::Unsubscribed {
117+ if let RoomUpdate::UnsubscribedFromRemoteAudioTrack {
120118 publisher_id,
121119 track_id,
122120 } = next
@@ -128,7 +126,6 @@ fn main() { · 5 unmodified lines
128126 panic!("unexpected message");
129127 }
130128
131− let mut video_track_updates = room_b.remote_video_track_updates();
132129 let displays = room_a.display_sources().await.unwrap();
133130 let display = displays.into_iter().next().unwrap();
134131
@@ -136,8 +133,8 @@ fn main() { · 1 unmodified line
136133 let local_video_track_publication =
137134 room_a.publish_video_track(local_video_track).await.unwrap();
138135
139− if let RemoteVideoTrackUpdate::Subscribed(track) =
140− video_track_updates.next().await.unwrap()
136+ if let RoomUpdate::SubscribedToRemoteVideoTrack(track) =
137+ room_updates.next().await.unwrap()
141138 {
142139 let remote_video_tracks = room_b.remote_video_tracks("test-participant-1");
143140 assert_eq!(remote_video_tracks.len(), 1);
@@ -152,10 +149,10 @@ fn main() { · 8 unmodified lines
152149 .pop()
153150 .unwrap();
154151 room_a.unpublish_track(local_video_track_publication);
155− if let RemoteVideoTrackUpdate::Unsubscribed {
152+ if let RoomUpdate::UnsubscribedFromRemoteVideoTrack {
156153 publisher_id,
157154 track_id,
158− } = video_track_updates.next().await.unwrap()
155+ } = room_updates.next().await.unwrap()
159156 {
160157 assert_eq!(publisher_id, "test-participant-1");
161158 assert_eq!(remote_video_track.sid(), track_id);
crates/live_kit_client/src/live_kit_client.rs+24−0
@@ -1,3 +1,5 @@
1+use std::sync::Arc;
2+
13 #[cfg(not(any(test, feature = "test-support")))]
24 pub mod prod;
35
@@ -9,3 +11,25 @@ pub mod test; · 5 unmodified lines
911
1012 #[cfg(any(test, feature = "test-support"))]
1113 pub use test::*;
14+
15+pub type Sid = String;
16+
17+#[derive(Clone, Eq, PartialEq)]
18+pub enum ConnectionState {
19+ Disconnected,
20+ Connected { url: String, token: String },
21+}
22+
23+#[derive(Clone)]
24+pub 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 @@
1+use crate::{ConnectionState, RoomUpdate, Sid};
12 use anyhow::{anyhow, Context, Result};
23 use core_foundation::{
34 array::{CFArray, CFArrayRef},
@@ -76,6 +77,16 @@ extern "C" { · 72 unmodified lines
7677 publisher_id: CFStringRef,
7778 track_id: CFStringRef,
7879 ),
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+ ),
7990 ) -> swift::RoomDelegate;
8091
8192 fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room;
@@ -151,26 +162,19 @@ extern "C" { · 69 unmodified lines
151162 callback_data: *mut c_void,
152163 );
153164
165+ fn LKLocalTrackPublicationIsMuted(publication: swift::LocalTrackPublication) -> bool;
154166 fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool;
167+ fn LKLocalTrackPublicationGetSid(publication: swift::LocalTrackPublication) -> CFStringRef;
155168 fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef;
156169 }
157170
158−pub type Sid = String;
159−
160−#[derive(Clone, Eq, PartialEq)]
161−pub enum ConnectionState {
162− Disconnected,
163− Connected { url: String, token: String },
164−}
165−
166171 pub struct Room {
167172 native_room: swift::Room,
168173 connection: Mutex<(
169174 watch::Sender<ConnectionState>,
170175 watch::Receiver<ConnectionState>,
171176 )>,
172− remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
173− remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
177+ update_subscribers: Mutex<Vec<mpsc::UnboundedSender<RoomUpdate>>>,
174178 _delegate: RoomDelegate,
175179 }
176180
@@ -181,8 +185,7 @@ impl Room { · 4 unmodified lines
181185 Self {
182186 native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
183187 connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
184− remote_audio_track_subscribers: Default::default(),
185− remote_video_track_subscribers: Default::default(),
188+ update_subscribers: Default::default(),
186189 _delegate: delegate,
187190 }
188191 })
@@ -397,15 +400,9 @@ impl Room { · 208 unmodified lines
397400 }
398401 }
399402
400− pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {
403+ pub fn updates(&self) -> mpsc::UnboundedReceiver<RoomUpdate> {
401404 let (tx, rx) = mpsc::unbounded();
402− self.remote_audio_track_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);
405+ self.update_subscribers.lock().push(tx);
409406 rx
410407 }
411408
@@ -416,8 +413,8 @@ impl Room { · 4 unmodified lines
416413 ) {
417414 let track = Arc::new(track);
418415 let publication = Arc::new(publication);
419− self.remote_audio_track_subscribers.lock().retain(|tx| {
420− tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(
416+ self.update_subscribers.lock().retain(|tx| {
417+ tx.unbounded_send(RoomUpdate::SubscribedToRemoteAudioTrack(
421418 track.clone(),
422419 publication.clone(),
423420 ))
@@ -426,8 +423,8 @@ impl Room { · 2 unmodified lines
426423 }
427424
428425 fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {
429− self.remote_audio_track_subscribers.lock().retain(|tx| {
430− tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed {
426+ self.update_subscribers.lock().retain(|tx| {
427+ tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteAudioTrack {
431428 publisher_id: publisher_id.clone(),
432429 track_id: track_id.clone(),
433430 })
@@ -436,8 +433,8 @@ impl Room { · 2 unmodified lines
436433 }
437434
438435 fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
439− self.remote_audio_track_subscribers.lock().retain(|tx| {
440− tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged {
436+ self.update_subscribers.lock().retain(|tx| {
437+ tx.unbounded_send(RoomUpdate::RemoteAudioTrackMuteChanged {
441438 track_id: track_id.clone(),
442439 muted,
443440 })
@@ -445,29 +442,26 @@ impl Room { · 1 unmodified line
445442 });
446443 }
447444
448− // A vec of publisher IDs
449445 fn active_speakers_changed(&self, speakers: Vec<String>) {
450− self.remote_audio_track_subscribers
451− .lock()
452− .retain(move |tx| {
453− tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged {
454− speakers: speakers.clone(),
455− })
456− .is_ok()
457− });
446+ self.update_subscribers.lock().retain(move |tx| {
447+ tx.unbounded_send(RoomUpdate::ActiveSpeakersChanged {
448+ speakers: speakers.clone(),
449+ })
450+ .is_ok()
451+ });
458452 }
459453
460454 fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
461455 let track = Arc::new(track);
462− self.remote_video_track_subscribers.lock().retain(|tx| {
463− tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone()))
456+ self.update_subscribers.lock().retain(|tx| {
457+ tx.unbounded_send(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
464458 .is_ok()
465459 });
466460 }
467461
468462 fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {
469− self.remote_video_track_subscribers.lock().retain(|tx| {
470− tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed {
463+ self.update_subscribers.lock().retain(|tx| {
464+ tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteVideoTrack {
471465 publisher_id: publisher_id.clone(),
472466 track_id: track_id.clone(),
473467 })
@@ -529,6 +523,8 @@ impl RoomDelegate { · 55 unmodified lines
529523 Self::on_active_speakers_changed,
530524 Self::on_did_subscribe_to_remote_video_track,
531525 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,
532528 )
533529 };
534530 Self {
@@ -642,6 +638,46 @@ impl RoomDelegate { · 107 unmodified lines
642638 }
643639 let _ = Weak::into_raw(room);
644640 }
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+ }
645681 }
646682
647683 impl Drop for RoomDelegate {
@@ -691,6 +727,10 @@ impl LocalTrackPublication { · 43 unmodified lines
691727 Self(native_track_publication)
692728 }
693729
730+ pub fn sid(&self) -> String {
731+ unsafe { CFString::wrap_under_get_rule(LKLocalTrackPublicationGetSid(self.0)).to_string() }
732+ }
733+
694734 pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
695735 let (tx, rx) = futures::channel::oneshot::channel();
696736
@@ -715,6 +755,19 @@ impl LocalTrackPublication { · 18 unmodified lines
715755
716756 async move { rx.await.unwrap() }
717757 }
758+
759+ pub fn is_muted(&self) -> bool {
760+ unsafe { LKLocalTrackPublicationIsMuted(self.0) }
761+ }
762+}
763+
764+impl Clone for LocalTrackPublication {
765+ fn clone(&self) -> Self {
766+ unsafe {
767+ CFRetain(self.0 .0);
768+ }
769+ Self(self.0)
770+ }
718771 }
719772
720773 impl Drop for LocalTrackPublication {
@@ -811,7 +864,11 @@ impl RemoteAudioTrack { · 90 unmodified lines
811864
812865 impl Drop for RemoteAudioTrack {
813866 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;
815872 }
816873 }
817874
@@ -889,18 +946,6 @@ impl Drop for RemoteVideoTrack { · 71 unmodified lines
889946 }
890947 }
891948
892−pub enum RemoteVideoTrackUpdate {
893− Subscribed(Arc<RemoteVideoTrack>),
894− Unsubscribed { publisher_id: Sid, track_id: Sid },
895−}
896−
897−pub 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−
904949 pub struct MacOSDisplay(swift::MacOSDisplay);
905950
906951 impl MacOSDisplay {
crates/live_kit_client/src/test.rs+65−61
@@ -1,3 +1,4 @@
1+use crate::{ConnectionState, RoomUpdate, Sid};
12 use anyhow::{anyhow, Context, Result};
23 use async_trait::async_trait;
34 use collections::{BTreeMap, HashMap};
@@ -7,7 +8,14 @@ use live_kit_server::{proto, token}; · 3 unmodified lines
78 use media::core_video::CVImageBuffer;
89 use parking_lot::Mutex;
910 use postage::watch;
10−use std::{future::Future, mem, sync::Arc};
11+use std::{
12+ future::Future,
13+ mem,
14+ sync::{
15+ atomic::{AtomicBool, Ordering::SeqCst},
16+ Arc,
17+ },
18+};
1119
1220 static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
1321
@@ -104,9 +112,8 @@ impl TestServer { · 90 unmodified lines
104112 client_room
105113 .0
106114 .lock()
107− .video_track_updates
108− .0
109− .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
115+ .updates_tx
116+ .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
110117 .unwrap();
111118 }
112119 room.client_rooms.insert(identity, client_room);
@@ -176,7 +183,11 @@ impl TestServer { · 63 unmodified lines
176183 }
177184 }
178185
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> {
180191 self.executor.simulate_random_delay().await;
181192 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
182193 let identity = claims.sub.unwrap().to_string();
@@ -198,8 +209,9 @@ impl TestServer { · 15 unmodified lines
198209 return Err(anyhow!("user is not allowed to publish"));
199210 }
200211
212+ let sid = nanoid::nanoid!(17);
201213 let track = Arc::new(RemoteVideoTrack {
202− sid: nanoid::nanoid!(17),
214+ sid: sid.clone(),
203215 publisher_id: identity.clone(),
204216 frames_rx: local_track.frames_rx.clone(),
205217 });
@@ -211,21 +223,20 @@ impl TestServer { · 5 unmodified lines
211223 let _ = client_room
212224 .0
213225 .lock()
214− .video_track_updates
215− .0
216− .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
226+ .updates_tx
227+ .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
217228 .unwrap();
218229 }
219230 }
220231
221− Ok(())
232+ Ok(sid)
222233 }
223234
224235 async fn publish_audio_track(
225236 &self,
226237 token: String,
227238 _local_track: &LocalAudioTrack,
228− ) -> Result<()> {
239+ ) -> Result<Sid> {
229240 self.executor.simulate_random_delay().await;
230241 let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
231242 let identity = claims.sub.unwrap().to_string();
@@ -247,8 +258,9 @@ impl TestServer { · 15 unmodified lines
247258 return Err(anyhow!("user is not allowed to publish"));
248259 }
249260
261+ let sid = nanoid::nanoid!(17);
250262 let track = Arc::new(RemoteAudioTrack {
251− sid: nanoid::nanoid!(17),
263+ sid: sid.clone(),
252264 publisher_id: identity.clone(),
253265 });
254266
@@ -261,9 +273,8 @@ impl TestServer { · 6 unmodified lines
261273 let _ = client_room
262274 .0
263275 .lock()
264− .audio_track_updates
265− .0
266− .try_broadcast(RemoteAudioTrackUpdate::Subscribed(
276+ .updates_tx
277+ .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
267278 track.clone(),
268279 publication.clone(),
269280 ))
@@ -271,7 +282,7 @@ impl TestServer { · 1 unmodified line
271282 }
272283 }
273284
274− Ok(())
285+ Ok(sid)
275286 }
276287
277288 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
369380 }
370381 }
371382
372−pub type Sid = String;
373−
374383 struct RoomState {
375384 connection: (
376385 watch::Sender<ConnectionState>,
377386 watch::Receiver<ConnectionState>,
378387 ),
379388 display_sources: Vec<MacOSDisplay>,
380− audio_track_updates: (
381− async_broadcast::Sender<RemoteAudioTrackUpdate>,
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)]
391−pub enum ConnectionState {
392− Disconnected,
393− Connected { url: String, token: String },
389+ updates_tx: async_broadcast::Sender<RoomUpdate>,
390+ updates_rx: async_broadcast::Receiver<RoomUpdate>,
394391 }
395392
396393 pub struct Room(Mutex<RoomState>);
397394
398395 impl Room {
399396 pub fn new() -> Arc<Self> {
397+ let (updates_tx, updates_rx) = async_broadcast::broadcast(128);
400398 Arc::new(Self(Mutex::new(RoomState {
401399 connection: watch::channel_with(ConnectionState::Disconnected),
402400 display_sources: Default::default(),
403− video_track_updates: async_broadcast::broadcast(128),
404− audio_track_updates: async_broadcast::broadcast(128),
401+ updates_tx,
402+ updates_rx,
405403 })))
406404 }
407405
@@ -440,10 +438,14 @@ impl Room { · 32 unmodified lines
440438 let this = self.clone();
441439 let track = track.clone();
442440 async move {
443− this.test_server()
441+ let sid = this
442+ .test_server()
444443 .publish_video_track(this.token(), track)
445444 .await?;
446− Ok(LocalTrackPublication)
445+ Ok(LocalTrackPublication {
446+ muted: Default::default(),
447+ sid,
448+ })
447449 }
448450 }
449451 pub fn publish_audio_track(
@@ -453,10 +455,14 @@ impl Room { · 3 unmodified lines
453455 let this = self.clone();
454456 let track = track.clone();
455457 async move {
456− this.test_server()
458+ let sid = this
459+ .test_server()
457460 .publish_audio_track(this.token(), &track)
458461 .await?;
459− Ok(LocalTrackPublication)
462+ Ok(LocalTrackPublication {
463+ muted: Default::default(),
464+ sid,
465+ })
460466 }
461467 }
462468
@@ -505,12 +511,8 @@ impl Room { · 42 unmodified lines
505511 .collect()
506512 }
507513
508− pub fn remote_audio_track_updates(&self) -> impl Stream<Item = RemoteAudioTrackUpdate> {
509− self.0.lock().audio_track_updates.1.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+ pub fn updates(&self) -> impl Stream<Item = RoomUpdate> {
515+ self.0.lock().updates_rx.clone()
514516 }
515517
516518 pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {
@@ -555,11 +557,27 @@ impl Drop for Room { · 38 unmodified lines
555557 }
556558 }
557559
558−pub struct LocalTrackPublication;
560+#[derive(Clone)]
561+pub struct LocalTrackPublication {
562+ sid: String,
563+ muted: Arc<AtomicBool>,
564+}
559565
560566 impl LocalTrackPublication {
561− pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {
562− async { Ok(()) }
567+ pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
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()
563581 }
564582 }
565583
@@ -646,20 +664,6 @@ impl RemoteAudioTrack { · 80 unmodified lines
646664 }
647665 }
648666
649−#[derive(Clone)]
650−pub enum RemoteVideoTrackUpdate {
651− Subscribed(Arc<RemoteVideoTrack>),
652− Unsubscribed { publisher_id: Sid, track_id: Sid },
653−}
654−
655−#[derive(Clone)]
656−pub 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−
663667 #[derive(Clone)]
664668 pub struct MacOSDisplay {
665669 frames: (
crates/project/src/project.rs+43−39
@@ -130,7 +130,7 @@ pub struct Project { · 129 unmodified lines
130130 next_diagnostic_group_id: usize,
131131 user_store: Model<UserStore>,
132132 fs: Arc<dyn Fs>,
133− client_state: Option<ProjectClientState>,
133+ client_state: ProjectClientState,
134134 collaborators: HashMap<proto::PeerId, Collaborator>,
135135 client_subscriptions: Vec<client::Subscription>,
136136 _subscriptions: Vec<gpui::Subscription>,
@@ -254,8 +254,10 @@ enum WorktreeHandle { · 117 unmodified lines
254254 Weak(WeakModel<Worktree>),
255255 }
256256
257+#[derive(Debug)]
257258 enum ProjectClientState {
258− Local {
259+ Local,
260+ Shared {
259261 remote_id: u64,
260262 updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,
261263 _send_updates: Task<Result<()>>,
@@ -657,7 +659,7 @@ impl Project { · 395 unmodified lines
657659 local_buffer_ids_by_entry_id: Default::default(),
658660 buffer_snapshots: Default::default(),
659661 join_project_response_message_id: 0,
660− client_state: None,
662+ client_state: ProjectClientState::Local,
661663 opened_buffer: watch::channel(),
662664 client_subscriptions: Vec::new(),
663665 _subscriptions: vec![
@@ -756,12 +758,12 @@ impl Project { · 92 unmodified lines
756758 cx.on_app_quit(Self::shutdown_language_servers),
757759 ],
758760 client: client.clone(),
759− client_state: Some(ProjectClientState::Remote {
761+ client_state: ProjectClientState::Remote {
760762 sharing_has_stopped: false,
761763 capability: Capability::ReadWrite,
762764 remote_id,
763765 replica_id,
764− }),
766+ },
765767 supplementary_language_servers: HashMap::default(),
766768 language_servers: Default::default(),
767769 language_server_ids: Default::default(),
@@ -828,16 +830,16 @@ impl Project { · 60 unmodified lines
828830
829831 fn release(&mut self, cx: &mut AppContext) {
830832 match &self.client_state {
831− Some(ProjectClientState::Local { .. }) => {
833+ ProjectClientState::Local => {}
834+ ProjectClientState::Shared { .. } => {
832835 let _ = self.unshare_internal(cx);
833836 }
834− Some(ProjectClientState::Remote { remote_id, .. }) => {
837+ ProjectClientState::Remote { remote_id, .. } => {
835838 let _ = self.client.send(proto::LeaveProject {
836839 project_id: *remote_id,
837840 });
838841 self.disconnected_from_host_internal(cx);
839842 }
840− _ => {}
841843 }
842844 }
843845
@@ -1058,21 +1060,22 @@ impl Project { · 214 unmodified lines
10581060 }
10591061
10601062 pub fn remote_id(&self) -> Option<u64> {
1061− match self.client_state.as_ref()? {
1062− ProjectClientState::Local { remote_id, .. }
1063− | ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
1063+ match self.client_state {
1064+ ProjectClientState::Local => None,
1065+ ProjectClientState::Shared { remote_id, .. }
1066+ | ProjectClientState::Remote { remote_id, .. } => Some(remote_id),
10641067 }
10651068 }
10661069
10671070 pub fn replica_id(&self) -> ReplicaId {
1068− match &self.client_state {
1069− Some(ProjectClientState::Remote { replica_id, .. }) => *replica_id,
1071+ match self.client_state {
1072+ ProjectClientState::Remote { replica_id, .. } => replica_id,
10701073 _ => 0,
10711074 }
10721075 }
10731076
10741077 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 {
10761079 updates_tx
10771080 .unbounded_send(LocalProjectUpdate::WorktreesChanged)
10781081 .ok();
@@ -1362,7 +1365,7 @@ impl Project { · 283 unmodified lines
13621365 }
13631366
13641367 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) {
13661369 return Err(anyhow!("project was already shared"));
13671370 }
13681371 self.client_subscriptions.push(
@@ -1423,7 +1426,7 @@ impl Project { · 54 unmodified lines
14231426
14241427 let (updates_tx, mut updates_rx) = mpsc::unbounded();
14251428 let client = self.client.clone();
1426− self.client_state = Some(ProjectClientState::Local {
1429+ self.client_state = ProjectClientState::Shared {
14271430 remote_id: project_id,
14281431 updates_tx,
14291432 _send_updates: cx.spawn(move |this, mut cx| async move {
@@ -1508,7 +1511,7 @@ impl Project { · 78 unmodified lines
15081511 }
15091512 Ok(())
15101513 }),
1511− });
1514+ };
15121515
15131516 self.metadata_changed(cx);
15141517 cx.emit(Event::RemoteIdChanged(Some(project_id)));
@@ -1578,7 +1581,8 @@ impl Project { · 63 unmodified lines
15781581 return Err(anyhow!("attempted to unshare a remote project"));
15791582 }
15801583
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;
15821586 self.collaborators.clear();
15831587 self.shared_buffers.clear();
15841588 self.client_subscriptions.clear();
@@ -1629,23 +1633,23 @@ impl Project { · 44 unmodified lines
16291633 } else {
16301634 Capability::ReadOnly
16311635 };
1632− if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
1636+ if let ProjectClientState::Remote { capability, .. } = &mut self.client_state {
16331637 if *capability == new_capability {
16341638 return;
16351639 }
16361640
16371641 *capability = new_capability;
1638− }
1639− for buffer in self.opened_buffers() {
1640− buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));
1642+ for buffer in self.opened_buffers() {
1643+ buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));
1644+ }
16411645 }
16421646 }
16431647
16441648 fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
1645− if let Some(ProjectClientState::Remote {
1649+ if let ProjectClientState::Remote {
16461650 sharing_has_stopped,
16471651 ..
1648− }) = &mut self.client_state
1652+ } = &mut self.client_state
16491653 {
16501654 *sharing_has_stopped = true;
16511655
@@ -1684,18 +1688,18 @@ impl Project { · 32 unmodified lines
16841688
16851689 pub fn is_disconnected(&self) -> bool {
16861690 match &self.client_state {
1687− Some(ProjectClientState::Remote {
1691+ ProjectClientState::Remote {
16881692 sharing_has_stopped,
16891693 ..
1690− }) => *sharing_has_stopped,
1694+ } => *sharing_has_stopped,
16911695 _ => false,
16921696 }
16931697 }
16941698
16951699 pub fn capability(&self) -> Capability {
16961700 match &self.client_state {
1697− Some(ProjectClientState::Remote { capability, .. }) => *capability,
1698− Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite,
1701+ ProjectClientState::Remote { capability, .. } => *capability,
1702+ ProjectClientState::Shared { .. } | ProjectClientState::Local => Capability::ReadWrite,
16991703 }
17001704 }
17011705
@@ -1705,8 +1709,8 @@ impl Project { · 3 unmodified lines
17051709
17061710 pub fn is_local(&self) -> bool {
17071711 match &self.client_state {
1708− Some(ProjectClientState::Remote { .. }) => false,
1709− _ => true,
1712+ ProjectClientState::Local | ProjectClientState::Shared { .. } => true,
1713+ ProjectClientState::Remote { .. } => false,
17101714 }
17111715 }
17121716
@@ -6165,8 +6169,8 @@ impl Project { · 4452 unmodified lines
61656169
61666170 pub fn is_shared(&self) -> bool {
61676171 match &self.client_state {
6168− Some(ProjectClientState::Local { .. }) => true,
6169− _ => false,
6172+ ProjectClientState::Shared { .. } => true,
6173+ ProjectClientState::Local | ProjectClientState::Remote { .. } => false,
61706174 }
61716175 }
61726176
@@ -7954,7 +7958,7 @@ impl Project { · 1781 unmodified lines
79547958 cx: &mut AppContext,
79557959 ) -> u64 {
79567960 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 {
79587962 updates_tx
79597963 .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id })
79607964 .ok();
@@ -8003,21 +8007,21 @@ impl Project { · 42 unmodified lines
80038007 }
80048008
80058009 fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
8006− let project_id = match self.client_state.as_ref() {
8007− Some(ProjectClientState::Remote {
8010+ let project_id = match self.client_state {
8011+ ProjectClientState::Remote {
80088012 sharing_has_stopped,
80098013 remote_id,
80108014 ..
8011− }) => {
8012− if *sharing_has_stopped {
8015+ } => {
8016+ if sharing_has_stopped {
80138017 return Task::ready(Err(anyhow!(
80148018 "can't synchronize remote buffers on a readonly project"
80158019 )));
80168020 } else {
8017− *remote_id
8021+ remote_id
80188022 }
80198023 }
8020− Some(ProjectClientState::Local { .. }) | None => {
8024+ ProjectClientState::Shared { .. } | ProjectClientState::Local => {
80218025 return Task::ready(Err(anyhow!(
80228026 "can't synchronize remote buffers on a local project"
80238027 )))
crates/search/src/buffer_search.rs+0−2
@@ -1130,7 +1130,6 @@ mod tests { · 1129 unmodified lines
11301130 #[gpui::test]
11311131 async fn test_search_simple(cx: &mut TestAppContext) {
11321132 let (editor, search_bar, cx) = init_test(cx);
1133− // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
11341133 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
11351134 background_highlights
11361135 .into_iter()
@@ -1395,7 +1394,6 @@ mod tests { · 258 unmodified lines
13951394 })
13961395 .await
13971396 .unwrap();
1398− // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
13991397 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
14001398 background_highlights
14011399 .into_iter()
crates/search/src/project_search.rs+119−3
@@ -1015,9 +1015,6 @@ impl ProjectSearchView { · 1014 unmodified lines
10151015 workspace.add_item(Box::new(view.clone()), cx);
10161016 view
10171017 };
1018−
1019− workspace.add_item(Box::new(search.clone()), cx);
1020−
10211018 search.update(cx, |search, cx| {
10221019 if let Some(query) = query {
10231020 search.set_query(&query, cx);
@@ -1984,6 +1981,7 @@ pub mod tests { · 960 unmodified lines
19841981 use semantic_index::semantic_index_settings::SemanticIndexSettings;
19851982 use serde_json::json;
19861983 use settings::{Settings, SettingsStore};
1984+ use workspace::DeploySearch;
19871985
19881986 #[gpui::test]
19891987 async fn test_project_search(cx: &mut TestAppContext) {
@@ -3111,6 +3109,124 @@ pub mod tests { · 1121 unmodified lines
31113109 .unwrap();
31123110 }
31133111
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+
31143230 pub fn init_test(cx: &mut TestAppContext) {
31153231 cx.update(|cx| {
31163232 let settings = SettingsStore::test(cx);
crates/terminal_view/src/terminal_element.rs+15−2
@@ -6,7 +6,7 @@ use gpui::{ · 5 unmodified lines
66 InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext,
77 ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point,
88 ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle,
9− WhiteSpace, WindowContext,
9+ WeakView, WhiteSpace, WindowContext,
1010 };
1111 use itertools::Itertools;
1212 use language::CursorShape;
@@ -24,6 +24,7 @@ use terminal::{ · 11 unmodified lines
2424 };
2525 use theme::{ActiveTheme, Theme, ThemeSettings};
2626 use ui::Tooltip;
27+use workspace::Workspace;
2728
2829 use std::mem;
2930 use std::{fmt::Debug, ops::RangeInclusive};
@@ -142,6 +143,7 @@ impl LayoutRect { · 112 unmodified lines
142143 ///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?
143144 pub struct TerminalElement {
144145 terminal: Model<Terminal>,
146+ workspace: WeakView<Workspace>,
145147 focus: FocusHandle,
146148 focused: bool,
147149 cursor_visible: bool,
@@ -160,6 +162,7 @@ impl StatefulInteractiveElement for TerminalElement {} · 12 unmodified lines
160162 impl TerminalElement {
161163 pub fn new(
162164 terminal: Model<Terminal>,
165+ workspace: WeakView<Workspace>,
163166 focus: FocusHandle,
164167 focused: bool,
165168 cursor_visible: bool,
@@ -167,6 +170,7 @@ impl TerminalElement { · 1 unmodified line
167170 ) -> TerminalElement {
168171 TerminalElement {
169172 terminal,
173+ workspace,
170174 focused,
171175 focus: focus.clone(),
172176 cursor_visible,
@@ -762,6 +766,7 @@ impl Element for TerminalElement { · 589 unmodified lines
762766 .cursor
763767 .as_ref()
764768 .map(|cursor| cursor.bounding_rect(origin)),
769+ workspace: self.workspace.clone(),
765770 };
766771
767772 self.register_mouse_listeners(origin, layout.mode, bounds, cx);
@@ -831,6 +836,7 @@ impl IntoElement for TerminalElement { · 63 unmodified lines
831836 struct TerminalInputHandler {
832837 cx: AsyncWindowContext,
833838 terminal: Model<Terminal>,
839+ workspace: WeakView<Workspace>,
834840 cursor_bounds: Option<Bounds<Pixels>>,
835841 }
836842
@@ -871,7 +877,14 @@ impl PlatformInputHandler for TerminalInputHandler { · 34 unmodified lines
871877 .update(|_, cx| {
872878 self.terminal.update(cx, |terminal, _| {
873879 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();
875888 })
876889 .ok();
877890 }
crates/terminal_view/src/terminal_view.rs+4−0
@@ -73,6 +73,7 @@ pub fn init(cx: &mut AppContext) { · 72 unmodified lines
7373 ///A terminal view, maintains the PTY's file handles and communicates with the terminal
7474 pub struct TerminalView {
7575 terminal: Model<Terminal>,
76+ workspace: WeakView<Workspace>,
7677 focus_handle: FocusHandle,
7778 has_new_content: bool,
7879 //Currently using iTerm bell, show bell emoji in tab until input is received
@@ -135,6 +136,7 @@ impl TerminalView { · 56 unmodified lines
135136 workspace_id: WorkspaceId,
136137 cx: &mut ViewContext<Self>,
137138 ) -> Self {
139+ let workspace_handle = workspace.clone();
138140 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
139141 cx.subscribe(&terminal, move |this, _, event, cx| match event {
140142 Event::Wakeup => {
@@ -279,6 +281,7 @@ impl TerminalView { · 138 unmodified lines
279281
280282 Self {
281283 terminal,
284+ workspace: workspace_handle,
282285 has_new_content: true,
283286 has_bell: false,
284287 focus_handle: cx.focus_handle(),
@@ -661,6 +664,7 @@ impl Render for TerminalView { · 376 unmodified lines
661664 // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
662665 div().size_full().child(TerminalElement::new(
663666 terminal_handle,
667+ self.workspace.clone(),
664668 self.focus_handle.clone(),
665669 focused,
666670 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
194194 ..Default::default()
195195 };
196196
197− root_schema
198− .definitions
199− .extend([("ThemeName".into(), theme_name_schema.into())]);
197+ let available_fonts = cx
198+ .text_system()
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+ ]);
200212
201213 root_schema
202214 .schema
@@ -204,10 +216,16 @@ impl settings::Settings for ThemeSettings { · 1 unmodified line
204216 .as_mut()
205217 .unwrap()
206218 .properties
207− .extend([(
208− "theme".to_owned(),
209− Schema::new_ref("#/definitions/ThemeName".into()),
210− )]);
219+ .extend([
220+ (
221+ "theme".to_owned(),
222+ Schema::new_ref("#/definitions/ThemeName".into()),
223+ ),
224+ (
225+ "buffer_font_family".to_owned(),
226+ Schema::new_ref("#/definitions/FontFamilies".into()),
227+ ),
228+ ]);
211229
212230 root_schema
213231 }
crates/theme/src/styles/players.rs+1−1
@@ -22,7 +22,7 @@ pub struct PlayerColors(pub Vec<PlayerColor>); · 21 unmodified lines
2222 impl Default for PlayerColors {
2323 /// Don't use this!
2424 /// 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"
2626 fn default() -> Self {
2727 Self::dark()
2828 }
crates/theme/src/themes/andromeda.rs+1−1
@@ -69,7 +69,7 @@ pub fn andromeda() -> UserThemeFamily { · 68 unmodified lines
6969 editor_highlighted_line_background: Some(rgba(0x21242bff).into()),
7070 editor_line_number: Some(rgba(0xf7f7f859).into()),
7171 editor_active_line_number: Some(rgba(0xf7f7f8ff).into()),
72− editor_invisible: Some(rgba(0xaca8aeff).into()),
72+ editor_invisible: Some(rgba(0x64646dff).into()),
7373 editor_wrap_guide: Some(rgba(0xf7f7f80d).into()),
7474 editor_active_wrap_guide: Some(rgba(0xf7f7f81a).into()),
7575 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
7070 editor_highlighted_line_background: Some(rgba(0x221f26ff).into()),
7171 editor_line_number: Some(rgba(0xefecf459).into()),
7272 editor_active_line_number: Some(rgba(0xefecf4ff).into()),
73− editor_invisible: Some(rgba(0x898591ff).into()),
73+ editor_invisible: Some(rgba(0x726c7aff).into()),
7474 editor_wrap_guide: Some(rgba(0xefecf40d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xefecf41a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x576dda1a).into()),
@@ -535,7 +535,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
535535 editor_highlighted_line_background: Some(rgba(0xe6e3ebff).into()),
536536 editor_line_number: Some(rgba(0x19171c59).into()),
537537 editor_active_line_number: Some(rgba(0x19171cff).into()),
538− editor_invisible: Some(rgba(0x5a5462ff).into()),
538+ editor_invisible: Some(rgba(0x726c7aff).into()),
539539 editor_wrap_guide: Some(rgba(0x19171c0d).into()),
540540 editor_active_wrap_guide: Some(rgba(0x19171c1a).into()),
541541 editor_document_highlight_read_background: Some(rgba(0x586dda1a).into()),
@@ -1000,7 +1000,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
10001000 editor_highlighted_line_background: Some(rgba(0x262622ff).into()),
10011001 editor_line_number: Some(rgba(0xfefbec59).into()),
10021002 editor_active_line_number: Some(rgba(0xfefbecff).into()),
1003− editor_invisible: Some(rgba(0xa4a08bff).into()),
1003+ editor_invisible: Some(rgba(0x8b8874ff).into()),
10041004 editor_wrap_guide: Some(rgba(0xfefbec0d).into()),
10051005 editor_active_wrap_guide: Some(rgba(0xfefbec1a).into()),
10061006 editor_document_highlight_read_background: Some(rgba(0x6684e01a).into()),
@@ -1465,7 +1465,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
14651465 editor_highlighted_line_background: Some(rgba(0xeeebd7ff).into()),
14661466 editor_line_number: Some(rgba(0x20201d59).into()),
14671467 editor_active_line_number: Some(rgba(0x20201dff).into()),
1468− editor_invisible: Some(rgba(0x706d5fff).into()),
1468+ editor_invisible: Some(rgba(0x8b8874ff).into()),
14691469 editor_wrap_guide: Some(rgba(0x20201d0d).into()),
14701470 editor_active_wrap_guide: Some(rgba(0x20201d1a).into()),
14711471 editor_document_highlight_read_background: Some(rgba(0x6784e01a).into()),
@@ -1930,7 +1930,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
19301930 editor_highlighted_line_background: Some(rgba(0x2c2b23ff).into()),
19311931 editor_line_number: Some(rgba(0xf4f3ec59).into()),
19321932 editor_active_line_number: Some(rgba(0xf4f3ecff).into()),
1933− editor_invisible: Some(rgba(0x91907fff).into()),
1933+ editor_invisible: Some(rgba(0x7a7867ff).into()),
19341934 editor_wrap_guide: Some(rgba(0xf4f3ec0d).into()),
19351935 editor_active_wrap_guide: Some(rgba(0xf4f3ec1a).into()),
19361936 editor_document_highlight_read_background: Some(rgba(0x37a1661a).into()),
@@ -2395,7 +2395,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
23952395 editor_highlighted_line_background: Some(rgba(0xebeae3ff).into()),
23962396 editor_line_number: Some(rgba(0x22221b59).into()),
23972397 editor_active_line_number: Some(rgba(0x22221bff).into()),
2398− editor_invisible: Some(rgba(0x61604fff).into()),
2398+ editor_invisible: Some(rgba(0x7a7867ff).into()),
23992399 editor_wrap_guide: Some(rgba(0x22221b0d).into()),
24002400 editor_active_wrap_guide: Some(rgba(0x22221b1a).into()),
24012401 editor_document_highlight_read_background: Some(rgba(0x38a1661a).into()),
@@ -2860,7 +2860,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
28602860 editor_highlighted_line_background: Some(rgba(0x27211eff).into()),
28612861 editor_line_number: Some(rgba(0xf1efee59).into()),
28622862 editor_active_line_number: Some(rgba(0xf1efeeff).into()),
2863− editor_invisible: Some(rgba(0xa79f9dff).into()),
2863+ editor_invisible: Some(rgba(0x89817eff).into()),
28642864 editor_wrap_guide: Some(rgba(0xf1efee0d).into()),
28652865 editor_active_wrap_guide: Some(rgba(0xf1efee1a).into()),
28662866 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),
@@ -3325,7 +3325,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
33253325 editor_highlighted_line_background: Some(rgba(0xe9e6e4ff).into()),
33263326 editor_line_number: Some(rgba(0x1b191859).into()),
33273327 editor_active_line_number: Some(rgba(0x1b1918ff).into()),
3328− editor_invisible: Some(rgba(0x6a6360ff).into()),
3328+ editor_invisible: Some(rgba(0x89817eff).into()),
33293329 editor_wrap_guide: Some(rgba(0x1b19180d).into()),
33303330 editor_active_wrap_guide: Some(rgba(0x1b19181a).into()),
33313331 editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()),
@@ -3790,7 +3790,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
37903790 editor_highlighted_line_background: Some(rgba(0x252025ff).into()),
37913791 editor_line_number: Some(rgba(0xf7f3f759).into()),
37923792 editor_active_line_number: Some(rgba(0xf7f3f7ff).into()),
3793− editor_invisible: Some(rgba(0xa99aa9ff).into()),
3793+ editor_invisible: Some(rgba(0x8b7c8bff).into()),
37943794 editor_wrap_guide: Some(rgba(0xf7f3f70d).into()),
37953795 editor_active_wrap_guide: Some(rgba(0xf7f3f71a).into()),
37963796 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),
@@ -4255,7 +4255,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
42554255 editor_highlighted_line_background: Some(rgba(0xe1d6e1ff).into()),
42564256 editor_line_number: Some(rgba(0x1b181b59).into()),
42574257 editor_active_line_number: Some(rgba(0x1b181bff).into()),
4258− editor_invisible: Some(rgba(0x6b5e6bff).into()),
4258+ editor_invisible: Some(rgba(0x8b7c8bff).into()),
42594259 editor_wrap_guide: Some(rgba(0x1b181b0d).into()),
42604260 editor_active_wrap_guide: Some(rgba(0x1b181b1a).into()),
42614261 editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()),
@@ -4720,7 +4720,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
47204720 editor_highlighted_line_background: Some(rgba(0x1c2529ff).into()),
47214721 editor_line_number: Some(rgba(0xebf8ff59).into()),
47224722 editor_active_line_number: Some(rgba(0xebf8ffff).into()),
4723− editor_invisible: Some(rgba(0x7ca0b3ff).into()),
4723+ editor_invisible: Some(rgba(0x66889aff).into()),
47244724 editor_wrap_guide: Some(rgba(0xebf8ff0d).into()),
47254725 editor_active_wrap_guide: Some(rgba(0xebf8ff1a).into()),
47264726 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),
@@ -5185,7 +5185,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
51855185 editor_highlighted_line_background: Some(rgba(0xcdeaf9ff).into()),
51865186 editor_line_number: Some(rgba(0x161b1d59).into()),
51875187 editor_active_line_number: Some(rgba(0x161b1dff).into()),
5188− editor_invisible: Some(rgba(0x526f7dff).into()),
5188+ editor_invisible: Some(rgba(0x66889aff).into()),
51895189 editor_wrap_guide: Some(rgba(0x161b1d0d).into()),
51905190 editor_active_wrap_guide: Some(rgba(0x161b1d1a).into()),
51915191 editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()),
@@ -5650,7 +5650,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
56505650 editor_highlighted_line_background: Some(rgba(0x252020ff).into()),
56515651 editor_line_number: Some(rgba(0xf4ecec59).into()),
56525652 editor_active_line_number: Some(rgba(0xf4ececff).into()),
5653− editor_invisible: Some(rgba(0x898383ff).into()),
5653+ editor_invisible: Some(rgba(0x726a6aff).into()),
56545654 editor_wrap_guide: Some(rgba(0xf4ecec0d).into()),
56555655 editor_active_wrap_guide: Some(rgba(0xf4ecec1a).into()),
56565656 editor_document_highlight_read_background: Some(rgba(0x7272ca1a).into()),
@@ -6115,7 +6115,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
61156115 editor_highlighted_line_background: Some(rgba(0xebe3e3ff).into()),
61166116 editor_line_number: Some(rgba(0x1b181859).into()),
61176117 editor_active_line_number: Some(rgba(0x1b1818ff).into()),
6118− editor_invisible: Some(rgba(0x5a5252ff).into()),
6118+ editor_invisible: Some(rgba(0x726a6aff).into()),
61196119 editor_wrap_guide: Some(rgba(0x1b18180d).into()),
61206120 editor_active_wrap_guide: Some(rgba(0x1b18181a).into()),
61216121 editor_document_highlight_read_background: Some(rgba(0x7372ca1a).into()),
@@ -6580,7 +6580,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
65806580 editor_highlighted_line_background: Some(rgba(0x1f2621ff).into()),
65816581 editor_line_number: Some(rgba(0xecf4ee59).into()),
65826582 editor_active_line_number: Some(rgba(0xecf4eeff).into()),
6583− editor_invisible: Some(rgba(0x859188ff).into()),
6583+ editor_invisible: Some(rgba(0x6c7a71ff).into()),
65846584 editor_wrap_guide: Some(rgba(0xecf4ee0d).into()),
65856585 editor_active_wrap_guide: Some(rgba(0xecf4ee1a).into()),
65866586 editor_document_highlight_read_background: Some(rgba(0x478c901a).into()),
@@ -7045,7 +7045,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
70457045 editor_highlighted_line_background: Some(rgba(0xe3ebe6ff).into()),
70467046 editor_line_number: Some(rgba(0x171c1959).into()),
70477047 editor_active_line_number: Some(rgba(0x171c19ff).into()),
7048− editor_invisible: Some(rgba(0x546259ff).into()),
7048+ editor_invisible: Some(rgba(0x6c7a71ff).into()),
70497049 editor_wrap_guide: Some(rgba(0x171c190d).into()),
70507050 editor_active_wrap_guide: Some(rgba(0x171c191a).into()),
70517051 editor_document_highlight_read_background: Some(rgba(0x488c901a).into()),
@@ -7510,7 +7510,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
75107510 editor_highlighted_line_background: Some(rgba(0x1f231fff).into()),
75117511 editor_line_number: Some(rgba(0xf4fbf459).into()),
75127512 editor_active_line_number: Some(rgba(0xf4fbf4ff).into()),
7513− editor_invisible: Some(rgba(0x8ba48bff).into()),
7513+ editor_invisible: Some(rgba(0x748b74ff).into()),
75147514 editor_wrap_guide: Some(rgba(0xf4fbf40d).into()),
75157515 editor_active_wrap_guide: Some(rgba(0xf4fbf41a).into()),
75167516 editor_document_highlight_read_background: Some(rgba(0x3e62f41a).into()),
@@ -7975,7 +7975,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
79757975 editor_highlighted_line_background: Some(rgba(0xdaeedaff).into()),
79767976 editor_line_number: Some(rgba(0x13151359).into()),
79777977 editor_active_line_number: Some(rgba(0x131513ff).into()),
7978− editor_invisible: Some(rgba(0x5f705fff).into()),
7978+ editor_invisible: Some(rgba(0x748b74ff).into()),
79797979 editor_wrap_guide: Some(rgba(0x1315130d).into()),
79807980 editor_active_wrap_guide: Some(rgba(0x1315131a).into()),
79817981 editor_document_highlight_read_background: Some(rgba(0x3f62f41a).into()),
@@ -8440,7 +8440,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
84408440 editor_highlighted_line_background: Some(rgba(0x262f51ff).into()),
84418441 editor_line_number: Some(rgba(0xf5f7ff59).into()),
84428442 editor_active_line_number: Some(rgba(0xf5f7ffff).into()),
8443− editor_invisible: Some(rgba(0x959bb2ff).into()),
8443+ editor_invisible: Some(rgba(0x7a819cff).into()),
84448444 editor_wrap_guide: Some(rgba(0xf5f7ff0d).into()),
84458445 editor_active_wrap_guide: Some(rgba(0xf5f7ff1a).into()),
84468446 editor_document_highlight_read_background: Some(rgba(0x3e8fd01a).into()),
@@ -8905,7 +8905,7 @@ pub fn atelier() -> UserThemeFamily { · 458 unmodified lines
89058905 editor_highlighted_line_background: Some(rgba(0xe5e8f5ff).into()),
89068906 editor_line_number: Some(rgba(0x20274659).into()),
89078907 editor_active_line_number: Some(rgba(0x202746ff).into()),
8908− editor_invisible: Some(rgba(0x606889ff).into()),
8908+ editor_invisible: Some(rgba(0x7a819cff).into()),
89098909 editor_wrap_guide: Some(rgba(0x2027460d).into()),
89108910 editor_active_wrap_guide: Some(rgba(0x2027461a).into()),
89118911 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
7070 editor_highlighted_line_background: Some(rgba(0x1f2127ff).into()),
7171 editor_line_number: Some(rgba(0xbfbdb659).into()),
7272 editor_active_line_number: Some(rgba(0xbfbdb6ff).into()),
73− editor_invisible: Some(rgba(0x8a8986ff).into()),
73+ editor_invisible: Some(rgba(0x666767ff).into()),
7474 editor_wrap_guide: Some(rgba(0xbfbdb60d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xbfbdb61a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x5ac2fe1a).into()),
@@ -514,7 +514,7 @@ pub fn ayu() -> UserThemeFamily { · 437 unmodified lines
514514 editor_highlighted_line_background: Some(rgba(0xececedff).into()),
515515 editor_line_number: Some(rgba(0x5c616659).into()),
516516 editor_active_line_number: Some(rgba(0x5c6166ff).into()),
517− editor_invisible: Some(rgba(0x8c8f93ff).into()),
517+ editor_invisible: Some(rgba(0xacafb1ff).into()),
518518 editor_wrap_guide: Some(rgba(0x5c61660d).into()),
519519 editor_active_wrap_guide: Some(rgba(0x5c61661a).into()),
520520 editor_document_highlight_read_background: Some(rgba(0x3b9ee51a).into()),
@@ -958,7 +958,7 @@ pub fn ayu() -> UserThemeFamily { · 437 unmodified lines
958958 editor_highlighted_line_background: Some(rgba(0x353944ff).into()),
959959 editor_line_number: Some(rgba(0xcccac259).into()),
960960 editor_active_line_number: Some(rgba(0xcccac2ff).into()),
961− editor_invisible: Some(rgba(0x9a9a98ff).into()),
961+ editor_invisible: Some(rgba(0x787a7cff).into()),
962962 editor_wrap_guide: Some(rgba(0xcccac20d).into()),
963963 editor_active_wrap_guide: Some(rgba(0xcccac21a).into()),
964964 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
7070 editor_highlighted_line_background: Some(rgba(0x3a3735ff).into()),
7171 editor_line_number: Some(rgba(0xfbf1c759).into()),
7272 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
73− editor_invisible: Some(rgba(0xc5b597ff).into()),
73+ editor_invisible: Some(rgba(0x928474ff).into()),
7474 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -521,7 +521,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
521521 editor_highlighted_line_background: Some(rgba(0x393634ff).into()),
522522 editor_line_number: Some(rgba(0xfbf1c759).into()),
523523 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
524− editor_invisible: Some(rgba(0xc5b597ff).into()),
524+ editor_invisible: Some(rgba(0x928474ff).into()),
525525 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
526526 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
527527 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -972,7 +972,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
972972 editor_highlighted_line_background: Some(rgba(0x3b3735ff).into()),
973973 editor_line_number: Some(rgba(0xfbf1c759).into()),
974974 editor_active_line_number: Some(rgba(0xfbf1c7ff).into()),
975− editor_invisible: Some(rgba(0xc5b597ff).into()),
975+ editor_invisible: Some(rgba(0x928474ff).into()),
976976 editor_wrap_guide: Some(rgba(0xfbf1c70d).into()),
977977 editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()),
978978 editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()),
@@ -1423,7 +1423,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
14231423 editor_highlighted_line_background: Some(rgba(0xecddb4ff).into()),
14241424 editor_line_number: Some(rgba(0x28282859).into()),
14251425 editor_active_line_number: Some(rgba(0x282828ff).into()),
1426− editor_invisible: Some(rgba(0x5f5650ff).into()),
1426+ editor_invisible: Some(rgba(0x928474ff).into()),
14271427 editor_wrap_guide: Some(rgba(0x2828280d).into()),
14281428 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
14291429 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),
@@ -1874,7 +1874,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
18741874 editor_highlighted_line_background: Some(rgba(0xecddb5ff).into()),
18751875 editor_line_number: Some(rgba(0x28282859).into()),
18761876 editor_active_line_number: Some(rgba(0x282828ff).into()),
1877− editor_invisible: Some(rgba(0x5f5650ff).into()),
1877+ editor_invisible: Some(rgba(0x928474ff).into()),
18781878 editor_wrap_guide: Some(rgba(0x2828280d).into()),
18791879 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
18801880 editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()),
@@ -2325,7 +2325,7 @@ pub fn gruvbox() -> UserThemeFamily { · 444 unmodified lines
23252325 editor_highlighted_line_background: Some(rgba(0xecdcb3ff).into()),
23262326 editor_line_number: Some(rgba(0x28282859).into()),
23272327 editor_active_line_number: Some(rgba(0x282828ff).into()),
2328− editor_invisible: Some(rgba(0x5f5650ff).into()),
2328+ editor_invisible: Some(rgba(0x928474ff).into()),
23292329 editor_wrap_guide: Some(rgba(0x2828280d).into()),
23302330 editor_active_wrap_guide: Some(rgba(0x2828281a).into()),
23312331 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
7070 editor_highlighted_line_background: Some(rgba(0x2f343eff).into()),
7171 editor_line_number: Some(rgba(0xc8ccd459).into()),
7272 editor_active_line_number: Some(rgba(0xc8ccd4ff).into()),
73− editor_invisible: Some(rgba(0x838994ff).into()),
73+ editor_invisible: Some(rgba(0x555a63ff).into()),
7474 editor_wrap_guide: Some(rgba(0xc8ccd40d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xc8ccd41a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x74ade81a).into()),
@@ -521,7 +521,7 @@ pub fn one() -> UserThemeFamily { · 444 unmodified lines
521521 editor_highlighted_line_background: Some(rgba(0xebebecff).into()),
522522 editor_line_number: Some(rgba(0x383a4159).into()),
523523 editor_active_line_number: Some(rgba(0x383a41ff).into()),
524− editor_invisible: Some(rgba(0x7f8188ff).into()),
524+ editor_invisible: Some(rgba(0xa3a3a4ff).into()),
525525 editor_wrap_guide: Some(rgba(0x383a410d).into()),
526526 editor_active_wrap_guide: Some(rgba(0x383a411a).into()),
527527 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
7070 editor_highlighted_line_background: Some(rgba(0x1d1b2aff).into()),
7171 editor_line_number: Some(rgba(0xe0def459).into()),
7272 editor_active_line_number: Some(rgba(0xe0def4ff).into()),
73− editor_invisible: Some(rgba(0x75718eff).into()),
73+ editor_invisible: Some(rgba(0x28253cff).into()),
7474 editor_wrap_guide: Some(rgba(0xe0def40d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()),
@@ -528,7 +528,7 @@ pub fn rose_pine() -> UserThemeFamily { · 451 unmodified lines
528528 editor_highlighted_line_background: Some(rgba(0xfef9f2ff).into()),
529529 editor_line_number: Some(rgba(0x57527959).into()),
530530 editor_active_line_number: Some(rgba(0x575279ff).into()),
531− editor_invisible: Some(rgba(0x706c8cff).into()),
531+ editor_invisible: Some(rgba(0x9691a4ff).into()),
532532 editor_wrap_guide: Some(rgba(0x5752790d).into()),
533533 editor_active_wrap_guide: Some(rgba(0x5752791a).into()),
534534 editor_document_highlight_read_background: Some(rgba(0x57949f1a).into()),
@@ -986,7 +986,7 @@ pub fn rose_pine() -> UserThemeFamily { · 451 unmodified lines
986986 editor_highlighted_line_background: Some(rgba(0x28253cff).into()),
987987 editor_line_number: Some(rgba(0xe0def459).into()),
988988 editor_active_line_number: Some(rgba(0xe0def4ff).into()),
989− editor_invisible: Some(rgba(0x85819eff).into()),
989+ editor_invisible: Some(rgba(0x595571ff).into()),
990990 editor_wrap_guide: Some(rgba(0xe0def40d).into()),
991991 editor_active_wrap_guide: Some(rgba(0xe0def41a).into()),
992992 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
6969 editor_highlighted_line_background: Some(rgba(0x2b3039ff).into()),
7070 editor_line_number: Some(rgba(0xfdf4c159).into()),
7171 editor_active_line_number: Some(rgba(0xfdf4c1ff).into()),
72− editor_invisible: Some(rgba(0xa69782ff).into()),
72+ editor_invisible: Some(rgba(0x7c6f64ff).into()),
7373 editor_wrap_guide: Some(rgba(0xfdf4c10d).into()),
7474 editor_active_wrap_guide: Some(rgba(0xfdf4c11a).into()),
7575 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
7070 editor_highlighted_line_background: Some(rgba(0x04313cff).into()),
7171 editor_line_number: Some(rgba(0xfdf6e359).into()),
7272 editor_active_line_number: Some(rgba(0xfdf6e3ff).into()),
73− editor_invisible: Some(rgba(0x93a1a1ff).into()),
73+ editor_invisible: Some(rgba(0x6d8288ff).into()),
7474 editor_wrap_guide: Some(rgba(0xfdf6e30d).into()),
7575 editor_active_wrap_guide: Some(rgba(0xfdf6e31a).into()),
7676 editor_document_highlight_read_background: Some(rgba(0x288bd11a).into()),
@@ -514,7 +514,7 @@ pub fn solarized() -> UserThemeFamily { · 437 unmodified lines
514514 editor_highlighted_line_background: Some(rgba(0xf3eddaff).into()),
515515 editor_line_number: Some(rgba(0x002b3659).into()),
516516 editor_active_line_number: Some(rgba(0x002b36ff).into()),
517− editor_invisible: Some(rgba(0x34555eff).into()),
517+ editor_invisible: Some(rgba(0x6d8288ff).into()),
518518 editor_wrap_guide: Some(rgba(0x002b360d).into()),
519519 editor_active_wrap_guide: Some(rgba(0x002b361a).into()),
520520 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
6969 editor_highlighted_line_background: Some(rgba(0x231f16ff).into()),
7070 editor_line_number: Some(rgba(0xf8f5de59).into()),
7171 editor_active_line_number: Some(rgba(0xf8f5deff).into()),
72− editor_invisible: Some(rgba(0x736e55ff).into()),
72+ editor_invisible: Some(rgba(0x494433ff).into()),
7373 editor_wrap_guide: Some(rgba(0xf8f5de0d).into()),
7474 editor_active_wrap_guide: Some(rgba(0xf8f5de1a).into()),
7575 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
240240 editor_highlighted_line_background: convert(editor.highlighted_line_background),
241241 editor_line_number: convert(editor.line_number),
242242 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),
244244 editor_wrap_guide: convert(editor.wrap_guide),
245245 editor_active_wrap_guide: convert(editor.active_wrap_guide),
246246 editor_document_highlight_read_background: convert(
crates/util/src/util.rs+11−0
@@ -137,6 +137,8 @@ pub trait ResultExt<E> { · 136 unmodified lines
137137 type Ok;
138138
139139 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;
140142 fn warn_on_err(self) -> Option<Self::Ok>;
141143 fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
142144 }
@@ -159,6 +161,14 @@ where · 16 unmodified lines
159161 }
160162 }
161163
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+
162172 fn warn_on_err(self) -> Option<T> {
163173 match self {
164174 Ok(value) => Some(value),
@@ -234,6 +244,7 @@ where · 69 unmodified lines
234244 }
235245 }
236246
247+#[must_use]
237248 pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
238249
239250 impl<F, T, E> Future for LogErrorFuture<F>
crates/vim/src/editor_events.rs+0−1
@@ -111,7 +111,6 @@ mod test { · 110 unmodified lines
111111
112112 let mut cx1 = VisualTestContext::from_window(cx.window, &cx);
113113 let editor1 = cx.editor.clone();
114− dbg!(editor1.entity_id());
115114
116115 let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
117116 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
66 };
77 use anyhow::Result;
88 use collections::{HashMap, HashSet, VecDeque};
9+use futures::{stream::FuturesUnordered, StreamExt};
910 use gpui::{
1011 actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
1112 AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,
@@ -255,8 +256,8 @@ impl Pane { · 243 unmodified lines
255256 let focus_handle = cx.focus_handle();
256257
257258 let subscriptions = vec![
258− cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)),
259− cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)),
259+ cx.on_focus_in(&focus_handle, Pane::focus_in),
260+ cx.on_focus_out(&focus_handle, Pane::focus_out),
260261 ];
261262
262263 let handle = cx.view().downgrade();
@@ -1796,23 +1797,46 @@ impl Pane { · 1533 unmodified lines
17961797 }
17971798 }
17981799 let mut to_pane = cx.view().clone();
1799− let split_direction = self.drag_split_direction;
1800+ let mut split_direction = self.drag_split_direction;
18001801 let paths = paths.paths().to_vec();
18011802 self.workspace
1802− .update(cx, |_, cx| {
1803− cx.defer(move |workspace, cx| {
1804− if let Some(split_direction) = split_direction {
1805− to_pane = workspace.split_pane(to_pane, split_direction, cx);
1803+ .update(cx, |workspace, cx| {
1804+ let fs = Arc::clone(workspace.project().read(cx).fs());
1805+ cx.spawn(|workspace, mut cx| async move {
1806+ let mut is_file_checks = FuturesUnordered::new();
1807+ for path in &paths {
1808+ is_file_checks.push(fs.is_file(path))
18061809 }
1807− workspace
1808− .open_paths(
1809− paths,
1810− OpenVisible::OnlyDirectories,
1811− Some(to_pane.downgrade()),
1812− cx,
1813− )
1814− .detach();
1815− });
1810+ let mut has_files_to_open = false;
1811+ while let Some(is_file) = is_file_checks.next().await {
1812+ if is_file {
1813+ has_files_to_open = true;
1814+ break;
1815+ }
1816+ }
1817+ drop(is_file_checks);
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();
18161840 })
18171841 .log_err();
18181842 }
crates/workspace/src/workspace.rs+427−363
@@ -512,6 +512,11 @@ impl Workspace { · 511 unmodified lines
512512
513513 project::Event::DisconnectedFromHost => {
514514 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+ }
515520 cx.disable_focus();
516521 }
517522
@@ -672,7 +677,7 @@ impl Workspace { · 154 unmodified lines
672677 // );
673678
674679 // this.show_notification(1, cx, |cx| {
675− // cx.build_view(|_cx| {
680+ // cx.new_view(|_cx| {
676681 // simple_message_notification::MessageNotification::new(format!("Error:"))
677682 // .with_click_message("click here because!")
678683 // })
@@ -2603,11 +2608,20 @@ impl Workspace { · 1924 unmodified lines
26032608 let cx = &cx;
26042609 move |item| {
26052610 let item = item.to_followable_item_handle(cx)?;
2606− if (project_id.is_none() || project_id != follower_project_id)
2607− && item.is_project_item(cx)
2611+
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)
26082621 {
26092622 return None;
26102623 }
2624+
26112625 let id = item.remote_id(client, cx)?.to_proto();
26122626 let variant = item.to_state_proto(cx)?;
26132627 Some(proto::View {
@@ -2785,8 +2799,12 @@ impl Workspace { · 171 unmodified lines
27852799 update: proto::update_followers::Variant,
27862800 cx: &mut WindowContext,
27872801 ) -> 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.
27882806 let project_id = if project_only {
2789− self.project.read(cx).remote_id()
2807+ Some(self.project.read(cx).remote_id()?)
27902808 } else {
27912809 None
27922810 };
@@ -4363,12 +4381,15 @@ mod tests { · 1570 unmodified lines
43634381 use std::{cell::RefCell, rc::Rc};
43644382
43654383 use super::*;
4366− use crate::item::{
4367− test::{TestItem, TestProjectItem},
4368− ItemEvent,
4384+ use crate::{
4385+ dock::{test::TestPanel, PanelEvent},
4386+ item::{
4387+ test::{TestItem, TestProjectItem},
4388+ ItemEvent,
4389+ },
43694390 };
43704391 use fs::FakeFs;
4371− use gpui::TestAppContext;
4392+ use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
43724393 use project::{Project, ProjectEntryId};
43734394 use serde_json::json;
43744395 use settings::SettingsStore;
@@ -4935,362 +4956,405 @@ mod tests { · 560 unmodified lines
49354956 });
49364957 }
49374958
4938− // #[gpui::test]
4939− // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4940− // init_test(cx);
4941− // 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− // }
4959+ #[gpui::test]
4960+ async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4961+ init_test(cx);
4962+ let fs = FakeFs::new(cx.executor());
50844963
5085− // #[gpui::test]
5086− // async fn test_panels(cx: &mut gpui::TestAppContext) {
5087− // init_test(cx);
5088− // let fs = FakeFs::new(cx.executor());
5089−
5090− // let project = Project::test(fs, [], cx).await;
5091− // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5092−
5093− // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5094− // // Add panel_1 on the left, panel_2 on the right.
5095− // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
5096− // workspace.add_panel(panel_1.clone(), cx);
5097− // workspace
5098− // .left_dock()
5099− // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5100− // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
5101− // workspace.add_panel(panel_2.clone(), cx);
5102− // workspace
5103− // .right_dock()
5104− // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5105−
5106− // let left_dock = workspace.left_dock();
5107− // assert_eq!(
5108− // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5109− // panel_1.panel_id()
5110− // );
5111− // assert_eq!(
5112− // left_dock.read(cx).active_panel_size(cx).unwrap(),
5113− // panel_1.size(cx)
5114− // );
5115−
5116− // left_dock.update(cx, |left_dock, cx| {
5117− // left_dock.resize_active_panel(Some(1337.), cx)
5118− // });
5119− // assert_eq!(
5120− // workspace
5121− // .right_dock()
5122− // .read(cx)
5123− // .visible_panel()
5124− // .unwrap()
5125− // .panel_id(),
5126− // panel_2.panel_id(),
5127− // );
5128−
5129− // (panel_1, panel_2)
5130− // });
5131−
5132− // // Move panel_1 to the right
5133− // panel_1.update(cx, |panel_1, cx| {
5134− // panel_1.set_position(DockPosition::Right, cx)
5135− // });
5136−
5137− // workspace.update(cx, |workspace, cx| {
5138− // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5139− // // Since it was the only panel on the left, the left dock should now be closed.
5140− // assert!(!workspace.left_dock().read(cx).is_open());
5141− // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5142− // let right_dock = workspace.right_dock();
5143− // assert_eq!(
5144− // right_dock.read(cx).visible_panel().unwrap().panel_id(),
5145− // panel_1.panel_id()
5146− // );
5147− // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5148−
5149− // // Now we move panel_2 to the left
5150− // panel_2.set_position(DockPosition::Left, cx);
5151− // });
5152−
5153− // workspace.update(cx, |workspace, cx| {
5154− // // Since panel_2 was not visible on the right, we don't open the left dock.
5155− // assert!(!workspace.left_dock().read(cx).is_open());
5156− // // And the right dock is unaffected in it's displaying of panel_1
5157− // assert!(workspace.right_dock().read(cx).is_open());
5158− // assert_eq!(
5159− // workspace
5160− // .right_dock()
5161− // .read(cx)
5162− // .visible_panel()
5163− // .unwrap()
5164− // .panel_id(),
5165− // panel_1.panel_id(),
5166− // );
5167− // });
5168−
5169− // // Move panel_1 back to the left
5170− // panel_1.update(cx, |panel_1, cx| {
5171− // panel_1.set_position(DockPosition::Left, cx)
5172− // });
5173−
5174− // workspace.update(cx, |workspace, cx| {
5175− // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5176− // let left_dock = workspace.left_dock();
5177− // assert!(left_dock.read(cx).is_open());
5178− // assert_eq!(
5179− // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5180− // panel_1.panel_id()
5181− // );
5182− // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5183− // // And right the dock should be closed as it no longer has any panels.
5184− // assert!(!workspace.right_dock().read(cx).is_open());
5185−
5186− // // Now we move panel_1 to the bottom
5187− // panel_1.set_position(DockPosition::Bottom, cx);
5188− // });
5189−
5190− // workspace.update(cx, |workspace, cx| {
5191− // // Since panel_1 was visible on the left, we close the left dock.
5192− // assert!(!workspace.left_dock().read(cx).is_open());
5193− // // The bottom dock is sized based on the panel's default size,
5194− // // since the panel orientation changed from vertical to horizontal.
5195− // let bottom_dock = workspace.bottom_dock();
5196− // assert_eq!(
5197− // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5198− // panel_1.size(cx),
5199− // );
5200− // // Close bottom dock and move panel_1 back to the left.
5201− // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5202− // panel_1.set_position(DockPosition::Left, cx);
5203− // });
5204−
5205− // // Emit activated event on panel 1
5206− // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5207−
5208− // // Now the left dock is open and panel_1 is active and focused.
5209− // workspace.update(cx, |workspace, cx| {
5210− // let left_dock = workspace.left_dock();
5211− // assert!(left_dock.read(cx).is_open());
5212− // assert_eq!(
5213− // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5214− // panel_1.panel_id(),
5215− // );
5216− // assert!(panel_1.focus_handle(cx).is_focused(cx));
5217− // });
5218−
5219− // // Emit closed event on panel 2, which is not active
5220− // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5221−
5222− // // Wo don't close the left dock, because panel_2 wasn't the active panel
5223− // workspace.update(cx, |workspace, cx| {
5224− // let left_dock = workspace.left_dock();
5225− // assert!(left_dock.read(cx).is_open());
5226− // assert_eq!(
5227− // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5228− // panel_1.panel_id(),
5229− // );
5230− // });
5231−
5232− // // Emitting a ZoomIn event shows the panel as zoomed.
5233− // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5234− // workspace.update(cx, |workspace, _| {
5235− // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5236− // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5237− // });
5238−
5239− // // Move panel to another dock while it is zoomed
5240− // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5241− // workspace.update(cx, |workspace, _| {
5242− // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5243−
5244− // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5245− // });
5246−
5247− // // If focus is transferred to another view that's not a panel or another pane, we still show
5248− // // the panel as zoomed.
5249− // let other_focus_handle = cx.update(|cx| cx.focus_handle());
5250− // cx.update(|cx| cx.focus(&other_focus_handle));
5251− // workspace.update(cx, |workspace, _| {
5252− // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5253− // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5254− // });
5255−
5256− // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5257− // workspace.update(cx, |_, cx| cx.focus_self());
5258− // workspace.update(cx, |workspace, _| {
5259− // assert_eq!(workspace.zoomed, None);
5260− // assert_eq!(workspace.zoomed_position, None);
5261− // });
5262−
5263− // // If focus is transferred again to another view that's not a panel or a pane, we won't
5264− // // show the panel as zoomed because it wasn't zoomed before.
5265− // cx.update(|cx| cx.focus(&other_focus_handle));
5266− // workspace.update(cx, |workspace, _| {
5267− // assert_eq!(workspace.zoomed, None);
5268− // assert_eq!(workspace.zoomed_position, None);
5269− // });
5270−
5271− // // When focus is transferred back to the panel, it is zoomed again.
5272− // panel_1.update(cx, |_, cx| cx.focus_self());
5273− // workspace.update(cx, |workspace, _| {
5274− // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5275− // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5276− // });
5277−
5278− // // Emitting a ZoomOut event unzooms the panel.
5279− // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5280− // workspace.update(cx, |workspace, _| {
5281− // assert_eq!(workspace.zoomed, None);
5282− // assert_eq!(workspace.zoomed_position, None);
5283− // });
5284−
5285− // // Emit closed event on panel 1, which is active
5286− // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5287−
5288− // // Now the left dock is closed, because panel_1 was the active panel
5289− // workspace.update(cx, |workspace, cx| {
5290− // let right_dock = workspace.right_dock();
5291− // assert!(!right_dock.read(cx).is_open());
5292− // });
5293− // }
4964+ let project = Project::test(fs, [], cx).await;
4965+ let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4966+
4967+ let panel = workspace.update(cx, |workspace, cx| {
4968+ let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
4969+ workspace.add_panel(panel.clone(), cx);
4970+
4971+ workspace
4972+ .right_dock()
4973+ .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4974+
4975+ panel
4976+ });
4977+
4978+ let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4979+ pane.update(cx, |pane, cx| {
4980+ let item = cx.new_view(|cx| TestItem::new(cx));
4981+ pane.add_item(Box::new(item), true, true, None, cx);
4982+ });
4983+
4984+ // Transfer focus from center to panel
4985+ workspace.update(cx, |workspace, cx| {
4986+ workspace.toggle_panel_focus::<TestPanel>(cx);
4987+ });
4988+
4989+ workspace.update(cx, |workspace, cx| {
4990+ assert!(workspace.right_dock().read(cx).is_open());
4991+ assert!(!panel.is_zoomed(cx));
4992+ assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4993+ });
4994+
4995+ // Transfer focus from panel to center
4996+ workspace.update(cx, |workspace, cx| {
4997+ workspace.toggle_panel_focus::<TestPanel>(cx);
4998+ });
4999+
5000+ workspace.update(cx, |workspace, cx| {
5001+ assert!(workspace.right_dock().read(cx).is_open());
5002+ assert!(!panel.is_zoomed(cx));
5003+ assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5004+ });
5005+
5006+ // Close the dock
5007+ workspace.update(cx, |workspace, cx| {
5008+ workspace.toggle_dock(DockPosition::Right, cx);
5009+ });
5010+
5011+ workspace.update(cx, |workspace, cx| {
5012+ assert!(!workspace.right_dock().read(cx).is_open());
5013+ assert!(!panel.is_zoomed(cx));
5014+ assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5015+ });
5016+
5017+ // Open the dock
5018+ workspace.update(cx, |workspace, cx| {
5019+ workspace.toggle_dock(DockPosition::Right, cx);
5020+ });
5021+
5022+ workspace.update(cx, |workspace, cx| {
5023+ assert!(workspace.right_dock().read(cx).is_open());
5024+ assert!(!panel.is_zoomed(cx));
5025+ assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5026+ });
5027+
5028+ // Focus and zoom panel
5029+ panel.update(cx, |panel, cx| {
5030+ cx.focus_self();
5031+ panel.set_zoomed(true, cx)
5032+ });
5033+
5034+ workspace.update(cx, |workspace, cx| {
5035+ assert!(workspace.right_dock().read(cx).is_open());
5036+ assert!(panel.is_zoomed(cx));
5037+ assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5038+ });
5039+
5040+ // Transfer focus to the center closes the dock
5041+ workspace.update(cx, |workspace, cx| {
5042+ workspace.toggle_panel_focus::<TestPanel>(cx);
5043+ });
5044+
5045+ workspace.update(cx, |workspace, cx| {
5046+ assert!(!workspace.right_dock().read(cx).is_open());
5047+ assert!(panel.is_zoomed(cx));
5048+ assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5049+ });
5050+
5051+ // Transferring focus back to the panel keeps it zoomed
5052+ workspace.update(cx, |workspace, cx| {
5053+ workspace.toggle_panel_focus::<TestPanel>(cx);
5054+ });
5055+
5056+ workspace.update(cx, |workspace, cx| {
5057+ assert!(workspace.right_dock().read(cx).is_open());
5058+ assert!(panel.is_zoomed(cx));
5059+ assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5060+ });
5061+
5062+ // Close the dock while it is zoomed
5063+ workspace.update(cx, |workspace, cx| {
5064+ workspace.toggle_dock(DockPosition::Right, cx)
5065+ });
5066+
5067+ workspace.update(cx, |workspace, cx| {
5068+ assert!(!workspace.right_dock().read(cx).is_open());
5069+ assert!(panel.is_zoomed(cx));
5070+ assert!(workspace.zoomed.is_none());
5071+ assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5072+ });
5073+
5074+ // Opening the dock, when it's zoomed, retains focus
5075+ workspace.update(cx, |workspace, cx| {
5076+ workspace.toggle_dock(DockPosition::Right, cx)
5077+ });
5078+
5079+ workspace.update(cx, |workspace, cx| {
5080+ assert!(workspace.right_dock().read(cx).is_open());
5081+ assert!(panel.is_zoomed(cx));
5082+ assert!(workspace.zoomed.is_some());
5083+ assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5084+ });
5085+
5086+ // Unzoom and close the panel, zoom the active pane.
5087+ panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5088+ workspace.update(cx, |workspace, cx| {
5089+ workspace.toggle_dock(DockPosition::Right, cx)
5090+ });
5091+ pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5092+
5093+ // Opening a dock unzooms the pane.
5094+ workspace.update(cx, |workspace, cx| {
5095+ workspace.toggle_dock(DockPosition::Right, cx)
5096+ });
5097+ workspace.update(cx, |workspace, cx| {
5098+ let pane = pane.read(cx);
5099+ assert!(!pane.is_zoomed());
5100+ assert!(!pane.focus_handle(cx).is_focused(cx));
5101+ assert!(workspace.right_dock().read(cx).is_open());
5102+ assert!(workspace.zoomed.is_none());
5103+ });
5104+ }
5105+
5106+ struct TestModal(FocusHandle);
5107+
5108+ impl TestModal {
5109+ fn new(cx: &mut ViewContext<Self>) -> Self {
5110+ Self(cx.focus_handle())
5111+ }
5112+ }
5113+
5114+ impl EventEmitter<DismissEvent> for TestModal {}
5115+
5116+ impl FocusableView for TestModal {
5117+ fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5118+ self.0.clone()
5119+ }
5120+ }
5121+
5122+ impl ModalView for TestModal {}
5123+
5124+ impl Render for TestModal {
5125+ fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5126+ div().track_focus(&self.0)
5127+ }
5128+ }
5129+
5130+ #[gpui::test]
5131+ async fn test_panels(cx: &mut gpui::TestAppContext) {
5132+ init_test(cx);
5133+ let fs = FakeFs::new(cx.executor());
5134+
5135+ let project = Project::test(fs, [], cx).await;
5136+ let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5137+
5138+ let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5139+ let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5140+ workspace.add_panel(panel_1.clone(), cx);
5141+ workspace
5142+ .left_dock()
5143+ .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5144+ let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5145+ workspace.add_panel(panel_2.clone(), cx);
5146+ workspace
5147+ .right_dock()
5148+ .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5149+
5150+ let left_dock = workspace.left_dock();
5151+ assert_eq!(
5152+ left_dock.read(cx).visible_panel().unwrap().panel_id(),
5153+ panel_1.panel_id()
5154+ );
5155+ assert_eq!(
5156+ left_dock.read(cx).active_panel_size(cx).unwrap(),
5157+ panel_1.size(cx)
5158+ );
5159+
5160+ left_dock.update(cx, |left_dock, cx| {
5161+ left_dock.resize_active_panel(Some(px(1337.)), cx)
5162+ });
5163+ assert_eq!(
5164+ workspace
5165+ .right_dock()
5166+ .read(cx)
5167+ .visible_panel()
5168+ .unwrap()
5169+ .panel_id(),
5170+ panel_2.panel_id(),
5171+ );
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+ }
52945358
52955359 pub fn init_test(cx: &mut TestAppContext) {
52965360 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
148148 cx.on_window_should_close(move |cx| {
149149 handle
150150 .update(cx, |workspace, cx| {
151+ // We'll handle closing asynchoronously
151152 workspace.close_window(&Default::default(), cx);
152153 false
153154 })
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
5656
5757 ### Environment Variables
5858
59−You 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.
59+You 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.
6060
6161 For 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).
6262
rust-toolchain.toml+2−1
@@ -1,4 +1,5 @@
11 [toolchain]
22 channel = "1.75"
3−components = [ "rustfmt" ]
3+profile = "minimal"
4+components = [ "rustfmt", "clippy" ]
45 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]
script/zed-local+36−11
@@ -1,24 +1,45 @@
11 #!/usr/bin/env node
22
3+const HELP = `
4+USAGE
5+ zed-local [options] [zed args]
6+
7+OPTIONS
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+
314 const { spawn, execFileSync } = require("child_process");
415
516 const RESOLUTION_REGEX = /(\d+) x (\d+)/;
617 const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
7−const RELEASE_MODE = "--release";
18+
19+let instanceCount = 1;
20+let isReleaseMode = false;
21+let isTop = false;
822
923 const args = process.argv.slice(2);
24+while (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+ }
1040
11−// Parse the number of Zed instances to spawn.
12−let instanceCount = 1;
13−const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX);
14−if (digitMatch) {
15− instanceCount = parseInt(digitMatch[1]);
1641 args.shift();
1742 }
18−const isReleaseMode = args.some((arg) => arg === RELEASE_MODE);
19−if (instanceCount > 4) {
20− throw new Error("Cannot spawn more than 4 instances");
21−}
2243
2344 // Parse the resolution of the main screen
2445 const displayInfo = JSON.parse(
@@ -34,7 +55,11 @@ if (!mainDisplayResolution) { · 9 unmodified lines
3455 throw new Error("Could not parse screen resolution");
3556 }
3657 const screenWidth = parseInt(mainDisplayResolution[1]);
37−const screenHeight = parseInt(mainDisplayResolution[2]);
58+let screenHeight = parseInt(mainDisplayResolution[2]);
59+
60+if (isTop) {
61+ screenHeight = Math.floor(screenHeight / 2);
62+}
3863
3964 // Determine the window size for each instance
4065 let instanceWidth = screenWidth;