diff --git a/.github/actions/check_formatting/action.yml b/.github/actions/check_formatting/action.yml deleted file mode 100644 index 7fef26407bd866babfb10c1aac6222968f54c1fd..0000000000000000000000000000000000000000 --- a/.github/actions/check_formatting/action.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: 'Check formatting' -description: 'Checks code formatting use cargo fmt' - -runs: - using: "composite" - steps: - - name: Install Rust - shell: bash -euxo pipefail {0} - run: | - rustup set profile minimal - rustup update stable - - - name: cargo fmt - shell: bash -euxo pipefail {0} - run: cargo fmt --all -- --check diff --git a/.github/actions/check_style/action.yml b/.github/actions/check_style/action.yml new file mode 100644 index 0000000000000000000000000000000000000000..5dc7c42b02f387fe76295e9ae110f28972ef8450 --- /dev/null +++ b/.github/actions/check_style/action.yml @@ -0,0 +1,17 @@ +name: "Check formatting" +description: "Checks code formatting use cargo fmt" + +runs: + using: "composite" + steps: + - name: cargo fmt + shell: bash -euxo pipefail {0} + run: cargo fmt --all -- --check + + - name: cargo clippy + shell: bash -euxo pipefail {0} + # clippy.toml is not currently supporting specifying allowed lints + # so specify those here, and disable the rest until Zed's workspace + # will have more fixes & suppression for the standard lint set + run: | + cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index 1ea51a06a6b4f26935e2c752beb0cad12139fcfb..af37af7fc429cc9311d033de6b22016ff1d3e24f 100644 --- a/.github/actions/run_tests/action.yml +++ b/.github/actions/run_tests/action.yml @@ -2,29 +2,26 @@ name: "Run tests" description: "Runs the tests" runs: - using: "composite" - steps: - - name: Install Rust - shell: bash -euxo pipefail {0} - run: | - rustup set profile minimal - rustup update stable - rustup target add wasm32-wasi - cargo install cargo-nextest + using: "composite" + steps: + - name: Install Rust + shell: bash -euxo pipefail {0} + run: | + cargo install cargo-nextest - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" - - name: Limit target directory size - shell: bash -euxo pipefail {0} - run: script/clear-target-dir-if-larger-than 100 + - name: Limit target directory size + shell: bash -euxo pipefail {0} + run: script/clear-target-dir-if-larger-than 100 - - name: Run check - shell: bash -euxo pipefail {0} - run: cargo check --tests --workspace + - name: Run check + shell: bash -euxo pipefail {0} + run: cargo check --tests --workspace - - name: Run tests - shell: bash -euxo pipefail {0} - run: cargo nextest run --workspace --no-fail-fast + - name: Run tests + shell: bash -euxo pipefail {0} + run: cargo nextest run --workspace --no-fail-fast diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ba25dbf947c14b485e7303cd12dd17f8a206927..476f263997d5caa7fb0f3cb576397b618347e82f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ env: RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting + style: + name: Check formatting and Clippy lints runs-on: - self-hosted - test @@ -33,19 +33,20 @@ jobs: with: clean: false submodules: "recursive" + fetch-depth: 0 - name: Set up default .cargo/config.toml run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml - - name: Run rustfmt - uses: ./.github/actions/check_formatting + - name: Run style checks + uses: ./.github/actions/check_style tests: name: Run tests runs-on: - self-hosted - test - needs: rustfmt + needs: style steps: - name: Checkout repo uses: actions/checkout@v3 @@ -75,14 +76,6 @@ jobs: APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi - - name: Install Node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index d1b8ddfdfbbe636c1f9817e134e0fc86871d5a03..a1704d58bdcffec9ce51779bf9af47b8be7fec13 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -3,41 +3,36 @@ name: Randomized Tests concurrency: randomized-tests on: - push: - branches: - - randomized-tests-runner - # schedule: - # - cron: '0 * * * *' + push: + branches: + - randomized-tests-runner + # schedule: + # - cron: '0 * * * *' env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 - ZED_SERVER_URL: https://zed.dev - ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }} + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 + ZED_SERVER_URL: https://zed.dev + ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }} jobs: - tests: - name: Run randomized tests - runs-on: - - self-hosted - - randomized-tests - steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable + tests: + name: Run randomized tests + runs-on: + - self-hosted + - randomized-tests + steps: + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: '18' + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: 'recursive' - - - name: Run randomized tests - run: script/randomized-test-ci + - name: Run randomized tests + run: script/randomized-test-ci diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index b7e6a0321e68b0ffe5c0521befcbfc13a76677a8..33ccb4cba93ce6dd6538a81426248c866c4ee81b 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -14,8 +14,8 @@ env: RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting + style: + name: Check formatting and Clippy lints runs-on: - self-hosted - test @@ -25,16 +25,17 @@ jobs: with: clean: false submodules: "recursive" + fetch-depth: 0 - - name: Run rustfmt - uses: ./.github/actions/check_formatting + - name: Run style checks + uses: ./.github/actions/check_style tests: name: Run tests runs-on: - self-hosted - test - needs: rustfmt + needs: style steps: - name: Checkout repo uses: actions/checkout@v3 @@ -59,14 +60,6 @@ jobs: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi - - name: Install Node uses: actions/setup-node@v3 with: diff --git a/Cargo.lock b/Cargo.lock index c6c028a3991bf3ecc955e2dd4f97d9fce8c455c2..8e57e74da02b6329eaaf5d12da8ff2a4aaf4aea1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.36.0" +version = "0.36.1" dependencies = [ "anyhow", "async-trait", @@ -2522,6 +2522,7 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "anyhow", "collections", "ctor", "editor", @@ -3440,40 +3441,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "include-flate" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0" -dependencies = [ - "include-flate-codegen-exports", - "lazy_static", - "libflate", -] - -[[package]] -name = "include-flate-codegen" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8" -dependencies = [ - "libflate", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "include-flate-codegen-exports" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03" -dependencies = [ - "include-flate-codegen", - "proc-macro-hack", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -3865,26 +3832,6 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" -[[package]] -name = "libflate" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" -dependencies = [ - "adler32", - "crc32fast", - "libflate_lz77", -] - -[[package]] -name = "libflate_lz77" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" -dependencies = [ - "rle-decode-fast", -] - [[package]] name = "libgit2-sys" version = "0.14.2+1.5.1" @@ -5462,12 +5409,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.67" @@ -6162,12 +6103,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rle-decode-fast" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" - [[package]] name = "rmp" version = "0.8.12" @@ -6315,7 +6250,6 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ - "include-flate", "rust-embed-impl", "rust-embed-utils", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index 7ea79f094c0b6d5d5cd4a1e63f70059db05ec6e3..79d28821d4b040f35fc1ab1dd391cddeee93bc47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,7 @@ prost = { version = "0.8" } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "8.0", features = ["include-exclude", "compression"] } +rust-embed = { version = "8.0", features = ["include-exclude"] } rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 3ff0db1a160e4d420a7aad83d56404e313ad3b46..1372f7a4157da475aa8ccbff8aeb940c85d2ea79 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -412,7 +412,8 @@ "cmd-shift-e": "project_panel::ToggleFocus", "cmd-?": "assistant::ToggleFocus", "cmd-alt-s": "workspace::SaveAll", - "cmd-k m": "language_selector::Toggle" + "cmd-k m": "language_selector::Toggle", + "escape": "workspace::Unfollow" } }, // Bindings from Sublime Text diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3561cc33852a84d78ed21371432743d9dc540862..87d2e9aa78df53676d003de0fd044cbab797bc5a 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -239,7 +239,8 @@ impl ActiveCall { if result.is_ok() { this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?; } else { - // TODO: Resport collaboration error + //TODO: report collaboration error + log::error!("invite failed: {:?}", result); } this.update(&mut cx, |this, cx| { @@ -282,7 +283,7 @@ impl ActiveCall { return Task::ready(Err(anyhow!("cannot join while on another call"))); } - let call = if let Some(call) = self.incoming_call.1.borrow().clone() { + let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() { call } else { return Task::ready(Err(anyhow!("no incoming call"))); diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 3d1f1e70c74ab481864884e37a33e6db03ba5b05..45c6c15fb00a4cc42131d7b3acfa8524201044c1 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -15,10 +15,7 @@ use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, }; use language::LanguageRegistry; -use live_kit_client::{ - LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, - RemoteVideoTrackUpdate, -}; +use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate}; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use settings::Settings as _; @@ -131,30 +128,11 @@ impl Room { } }); - let _maintain_video_tracks = cx.spawn({ + let _handle_updates = cx.spawn({ let room = room.clone(); move |this, mut cx| async move { - let mut track_video_changes = room.remote_video_track_updates(); - while let Some(track_change) = track_video_changes.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - - this.update(&mut cx, |this, cx| { - this.remote_video_track_updated(track_change, cx).log_err() - }) - .ok(); - } - } - }); - - let _maintain_audio_tracks = cx.spawn({ - let room = room.clone(); - |this, mut cx| async move { - let mut track_audio_changes = room.remote_audio_track_updates(); - while let Some(track_change) = track_audio_changes.next().await { + let mut updates = room.updates(); + while let Some(update) = updates.next().await { let this = if let Some(this) = this.upgrade() { this } else { @@ -162,7 +140,7 @@ impl Room { }; this.update(&mut cx, |this, cx| { - this.remote_audio_track_updated(track_change, cx).log_err() + this.live_kit_room_updated(update, cx).log_err() }) .ok(); } @@ -195,7 +173,7 @@ impl Room { deafened: false, speaking: false, _maintain_room, - _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks], + _handle_updates, }) } else { None @@ -877,8 +855,8 @@ impl Room { .remote_audio_track_publications(&user.id.to_string()); for track in video_tracks { - this.remote_video_track_updated( - RemoteVideoTrackUpdate::Subscribed(track), + this.live_kit_room_updated( + RoomUpdate::SubscribedToRemoteVideoTrack(track), cx, ) .log_err(); @@ -887,8 +865,8 @@ impl Room { for (track, publication) in audio_tracks.iter().zip(publications.iter()) { - this.remote_audio_track_updated( - RemoteAudioTrackUpdate::Subscribed( + this.live_kit_room_updated( + RoomUpdate::SubscribedToRemoteAudioTrack( track.clone(), publication.clone(), ), @@ -979,13 +957,13 @@ impl Room { } } - fn remote_video_track_updated( + fn live_kit_room_updated( &mut self, - change: RemoteVideoTrackUpdate, + update: RoomUpdate, cx: &mut ModelContext, ) -> Result<()> { - match change { - RemoteVideoTrackUpdate::Subscribed(track) => { + match update { + RoomUpdate::SubscribedToRemoteVideoTrack(track) => { let user_id = track.publisher_id().parse()?; let track_id = track.sid().to_string(); let participant = self @@ -997,7 +975,8 @@ impl Room { participant_id: participant.peer_id, }); } - RemoteVideoTrackUpdate::Unsubscribed { + + RoomUpdate::UnsubscribedFromRemoteVideoTrack { publisher_id, track_id, } => { @@ -1011,19 +990,8 @@ impl Room { participant_id: participant.peer_id, }); } - } - cx.notify(); - Ok(()) - } - - fn remote_audio_track_updated( - &mut self, - change: RemoteAudioTrackUpdate, - cx: &mut ModelContext, - ) -> Result<()> { - match change { - RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => { + RoomUpdate::ActiveSpeakersChanged { speakers } => { let mut speaker_ids = speakers .into_iter() .filter_map(|speaker_sid| speaker_sid.parse().ok()) @@ -1045,9 +1013,9 @@ impl Room { } } } - cx.notify(); } - RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => { + + RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } => { let mut found = false; for participant in &mut self.remote_participants.values_mut() { for track in participant.audio_tracks.values() { @@ -1061,10 +1029,9 @@ impl Room { break; } } - - cx.notify(); } - RemoteAudioTrackUpdate::Subscribed(track, publication) => { + + RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => { let user_id = track.publisher_id().parse()?; let track_id = track.sid().to_string(); let participant = self @@ -1078,7 +1045,8 @@ impl Room { participant_id: participant.peer_id, }); } - RemoteAudioTrackUpdate::Unsubscribed { + + RoomUpdate::UnsubscribedFromRemoteAudioTrack { publisher_id, track_id, } => { @@ -1092,6 +1060,28 @@ impl Room { participant_id: participant.peer_id, }); } + + RoomUpdate::LocalAudioTrackUnpublished { publication } => { + log::info!("unpublished audio track {}", publication.sid()); + if let Some(room) = &mut self.live_kit { + room.microphone_track = LocalTrack::None; + } + } + + RoomUpdate::LocalVideoTrackUnpublished { publication } => { + log::info!("unpublished video track {}", publication.sid()); + if let Some(room) = &mut self.live_kit { + room.screen_track = LocalTrack::None; + } + } + + RoomUpdate::LocalAudioTrackPublished { publication } => { + log::info!("published audio track {}", publication.sid()); + } + + RoomUpdate::LocalVideoTrackPublished { publication } => { + log::info!("published video track {}", publication.sid()); + } } cx.notify(); @@ -1235,7 +1225,12 @@ impl Room { }; self.client.send(proto::UnshareProject { project_id })?; - project.update(cx, |this, cx| this.unshare(cx)) + project.update(cx, |this, cx| this.unshare(cx))?; + + if self.local_participant.active_project == Some(project.downgrade()) { + self.set_location(Some(&project), cx).detach_and_log_err(cx); + } + Ok(()) } pub(crate) fn set_location( @@ -1597,7 +1592,7 @@ struct LiveKitRoom { speaking: bool, next_publish_id: usize, _maintain_room: Task<()>, - _maintain_tracks: [Task<()>; 2], + _handle_updates: Task<()>, } impl LiveKitRoom { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 26b5748187ff735ebedf2cdb38753bdb6d9fcedc..aa0c7c4af58be6c181a04e2b73cff55230096bf9 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,3 +1,5 @@ +mod event_coalescer; + use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use chrono::{DateTime, Utc}; use futures::Future; @@ -5,7 +7,6 @@ use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; -use serde_json; use settings::{Settings, SettingsStore}; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ @@ -15,6 +16,8 @@ use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; +use self::event_coalescer::EventCoalescer; + pub struct Telemetry { http_client: Arc, executor: BackgroundExecutor, @@ -34,6 +37,7 @@ struct TelemetryState { log_file: Option, is_staff: Option, first_event_datetime: Option>, + event_coalescer: EventCoalescer, } const EVENTS_URL_PATH: &'static str = "/api/events"; @@ -118,19 +122,24 @@ pub enum Event { value: String, milliseconds_since_first_event: i64, }, + Edit { + duration: i64, + environment: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] -const MAX_QUEUE_LEN: usize = 1; +const MAX_QUEUE_LEN: usize = 5; #[cfg(not(debug_assertions))] const MAX_QUEUE_LEN: usize = 50; #[cfg(debug_assertions)] -const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); +const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); #[cfg(not(debug_assertions))] -const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5); +const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5); impl Telemetry { pub fn new(client: Arc, cx: &mut AppContext) -> Arc { @@ -150,11 +159,12 @@ impl Telemetry { installation_id: None, metrics_id: None, session_id: None, - events_queue: Default::default(), - flush_events_task: Default::default(), + events_queue: Vec::new(), + flush_events_task: None, log_file: None, is_staff: None, first_event_datetime: None, + event_coalescer: EventCoalescer::new(), })); cx.observe_global::({ @@ -194,7 +204,7 @@ impl Telemetry { #[cfg(not(any(test, feature = "test-support")))] fn shutdown_telemetry(self: &Arc) -> impl Future { self.report_app_event("close"); - self.flush_events(); + // TODO: close final edit period and make sure it's sent Task::ready(()) } @@ -392,6 +402,22 @@ impl Telemetry { } } + pub fn log_edit_event(self: &Arc, environment: &'static str) { + let mut state = self.state.lock(); + let period_data = state.event_coalescer.log_event(environment); + drop(state); + + if let Some((start, end, environment)) = period_data { + let event = Event::Edit { + duration: end.timestamp_millis() - start.timestamp_millis(), + environment, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_event(event); + } + } + fn report_event(self: &Arc, event: Event) { let mut state = self.state.lock(); @@ -410,7 +436,7 @@ impl Telemetry { let this = self.clone(); let executor = self.executor.clone(); state.flush_events_task = Some(self.executor.spawn(async move { - executor.timer(DEBOUNCE_INTERVAL).await; + executor.timer(FLUSH_DEBOUNCE_INTERVAL).await; this.flush_events(); })); } @@ -435,6 +461,9 @@ impl Telemetry { let mut events = mem::take(&mut state.events_queue); state.flush_events_task.take(); drop(state); + if events.is_empty() { + return; + } let this = self.clone(); self.executor diff --git a/crates/client/src/telemetry/event_coalescer.rs b/crates/client/src/telemetry/event_coalescer.rs new file mode 100644 index 0000000000000000000000000000000000000000..f0efeb38e6685c94bfb2da7395a91ef168f41cd9 --- /dev/null +++ b/crates/client/src/telemetry/event_coalescer.rs @@ -0,0 +1,279 @@ +use chrono::{DateTime, Duration, Utc}; +use std::time; + +const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20); +const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1); + +#[derive(Debug, PartialEq)] +struct PeriodData { + environment: &'static str, + start: DateTime, + end: Option>, +} + +pub struct EventCoalescer { + state: Option, +} + +impl EventCoalescer { + pub fn new() -> Self { + Self { state: None } + } + + pub fn log_event( + &mut self, + environment: &'static str, + ) -> Option<(DateTime, DateTime, &'static str)> { + self.log_event_with_time(Utc::now(), environment) + } + + // pub fn close_current_period(&mut self) -> Option<(DateTime, DateTime)> { + // self.environment.map(|env| self.log_event(env)).flatten() + // } + + fn log_event_with_time( + &mut self, + log_time: DateTime, + environment: &'static str, + ) -> Option<(DateTime, DateTime, &'static str)> { + let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap(); + + let Some(state) = &mut self.state else { + self.state = Some(PeriodData { + start: log_time, + end: None, + environment, + }); + return None; + }; + + let period_end = state + .end + .unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT); + let within_timeout = log_time - period_end < coalesce_timeout; + let environment_is_same = state.environment == environment; + let should_coaelesce = !within_timeout || !environment_is_same; + + if should_coaelesce { + let previous_environment = state.environment; + let original_start = state.start; + + state.start = log_time; + state.end = None; + state.environment = environment; + + return Some(( + original_start, + if within_timeout { log_time } else { period_end }, + previous_environment, + )); + } + + state.end = Some(log_time); + + None + } +} + +#[cfg(test)] +mod tests { + use chrono::TimeZone; + + use super::*; + + #[test] + fn test_same_context_exceeding_timeout() { + let environment_1 = "environment_1"; + let mut event_coalescer = EventCoalescer::new(); + + assert_eq!(event_coalescer.state, None); + + let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); + let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: None, + environment: environment_1, + }) + ); + + let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let mut period_end = period_start; + + // Ensure that many calls within the timeout don't start a new period + for _ in 0..100 { + period_end += within_timeout_adjustment; + let period_data = event_coalescer.log_event_with_time(period_end, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: Some(period_end), + environment: environment_1, + }) + ); + } + + let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + // Logging an event exceeding the timeout should start a new period + let new_period_start = period_end + exceed_timeout_adjustment; + let period_data = event_coalescer.log_event_with_time(new_period_start, environment_1); + + assert_eq!(period_data, Some((period_start, period_end, environment_1))); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: new_period_start, + end: None, + environment: environment_1, + }) + ); + } + + #[test] + fn test_different_environment_under_timeout() { + let environment_1 = "environment_1"; + let mut event_coalescer = EventCoalescer::new(); + + assert_eq!(event_coalescer.state, None); + + let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); + let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: None, + environment: environment_1, + }) + ); + + let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let period_end = period_start + within_timeout_adjustment; + let period_data = event_coalescer.log_event_with_time(period_end, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: Some(period_end), + environment: environment_1, + }) + ); + + // Logging an event within the timeout but with a different environment should start a new period + let period_end = period_end + within_timeout_adjustment; + let environment_2 = "environment_2"; + let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + + assert_eq!(period_data, Some((period_start, period_end, environment_1))); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_end, + end: None, + environment: environment_2, + }) + ); + } + + #[test] + fn test_switching_environment_while_within_timeout() { + let environment_1 = "environment_1"; + let mut event_coalescer = EventCoalescer::new(); + + assert_eq!(event_coalescer.state, None); + + let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); + let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: None, + environment: environment_1, + }) + ); + + let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let period_end = period_start + within_timeout_adjustment; + let environment_2 = "environment_2"; + let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + + assert_eq!(period_data, Some((period_start, period_end, environment_1))); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_end, + end: None, + environment: environment_2, + }) + ); + } + // // 0 20 40 60 + // // |-------------------|-------------------|-------------------|------------------- + // // |--------|----------env change + // // |------------------- + // // |period_start |period_end + // // |new_period_start + + #[test] + fn test_switching_environment_while_exceeding_timeout() { + let environment_1 = "environment_1"; + let mut event_coalescer = EventCoalescer::new(); + + assert_eq!(event_coalescer.state, None); + + let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); + let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + + assert_eq!(period_data, None); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_start, + end: None, + environment: environment_1, + }) + ); + + let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + let period_end = period_start + exceed_timeout_adjustment; + let environment_2 = "environment_2"; + let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + + assert_eq!( + period_data, + Some(( + period_start, + period_start + SIMULATED_DURATION_FOR_SINGLE_EVENT, + environment_1 + )) + ); + assert_eq!( + event_coalescer.state, + Some(PeriodData { + start: period_end, + end: None, + environment: environment_2, + }) + ); + } + // 0 20 40 60 + // |-------------------|-------------------|-------------------|------------------- + // |--------|----------------------------------------env change + // |-------------------| + // |period_start |period_end + // |new_period_start +} diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index b0917104f98bc30d193e21171aca81f56199c83b..bc273cb12a9b893ac0e0b7d0b710416ea4ba37b8 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.36.0" +version = "0.36.1" publish = false [[bin]] diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 9bbbf88dac9879bf12dee3c99c35c8b18ca8d527..507cf197f70b9bcce2c74a16b1c6ead569965a5d 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -37,7 +37,7 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); CREATE TABLE "rooms" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "live_kit_room" VARCHAR NOT NULL, - "enviroment" VARCHAR, + "environment" VARCHAR, "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab/migrations/20240111085546_fix_column_name.sql b/crates/collab/migrations/20240111085546_fix_column_name.sql new file mode 100644 index 0000000000000000000000000000000000000000..3f32ee35c59107e12fda98159911dbba6e13434a --- /dev/null +++ b/crates/collab/migrations/20240111085546_fix_column_name.sql @@ -0,0 +1 @@ +ALTER TABLE rooms ADD COLUMN environment TEXT; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 9c28e998c95426bc1026bcc5df86e8c528c4da8b..6243b03bf7ad2f331b3ede34c2390db574940546 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1180,7 +1180,7 @@ impl Database { .await?; let room_id = if let Some(room) = room { - if let Some(env) = room.enviroment { + if let Some(env) = room.environment { if &env != environment { Err(anyhow!("must join using the {} release", env))?; } @@ -1190,7 +1190,7 @@ impl Database { let result = room::Entity::insert(room::ActiveModel { channel_id: ActiveValue::Set(Some(channel_id)), live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - enviroment: ActiveValue::Set(Some(environment.to_string())), + environment: ActiveValue::Set(Some(environment.to_string())), ..Default::default() }) .exec(&*tx) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index c3e71880fdc5e8eb871dfd9a9aff3e85251a321e..178cb712ed5df6f0bcaaed45e5fd4d43996d88b1 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -112,7 +112,7 @@ impl Database { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), - enviroment: ActiveValue::set(Some(release_channel.to_string())), + environment: ActiveValue::set(Some(release_channel.to_string())), ..Default::default() } .insert(&*tx) @@ -299,28 +299,28 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, - enviroment: &str, + environment: &str, ) -> Result> { self.room_transaction(room_id, |tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelIdAndEnviroment { + enum QueryChannelIdAndEnvironment { ChannelId, - Enviroment, + Environment, } let (channel_id, release_channel): (Option, Option) = room::Entity::find() .select_only() .column(room::Column::ChannelId) - .column(room::Column::Enviroment) + .column(room::Column::Environment) .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelIdAndEnviroment>() + .into_values::<_, QueryChannelIdAndEnvironment>() .one(&*tx) .await? .ok_or_else(|| anyhow!("no such room"))?; if let Some(release_channel) = release_channel { - if &release_channel != enviroment { + if &release_channel != environment { Err(anyhow!("must join using the {} release", release_channel))?; } } diff --git a/crates/collab/src/db/tables/room.rs b/crates/collab/src/db/tables/room.rs index 4150c741ac19ef39e09c19116ba3bca819e24a3f..f75a079317311f3595279d46b41234103afc1294 100644 --- a/crates/collab/src/db/tables/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -8,7 +8,7 @@ pub struct Model { pub id: RoomId, pub live_kit_room: String, pub channel_id: Option, - pub enviroment: Option, + pub environment: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 9b68ce3922ab24726130c356636dae7d6899ef35..d5933235926fa1da8c1e8538dedd2a7506504cd8 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -104,12 +104,10 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }); assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); - assert!(dbg!( - room_b - .update(cx_b, |room, cx| room.share_microphone(cx)) - .await - ) - .is_err()); + assert!(room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + .is_err()); // B is promoted active_call_a diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 5870bd193842620a2953e577ab0005b48237bcde..e59aa3c705fd7b19c77b0a3c231fed5486a72786 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,7 +1,9 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::{ChannelChat, ChannelMessageId, MessageParams}; +use collab_ui::chat_panel::ChatPanel; use gpui::{BackgroundExecutor, Model, TestAppContext}; use rpc::Notification; +use workspace::dock::Panel; #[gpui::test] async fn test_basic_channel_messages( @@ -273,135 +275,135 @@ fn assert_messages(chat: &Model, messages: &[&str], cx: &mut TestAp ); } -//todo!(collab_ui) -// #[gpui::test] -// async fn test_channel_message_changes( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// // Client A sends a message, client B should see that there is a new message. -// let channel_chat_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) -// .await -// .unwrap(); - -// channel_chat_a -// .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let b_has_messages = cx_b.read_with(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_new_messages(channel_id) -// .unwrap() -// }); - -// assert!(b_has_messages); - -// // Opening the chat should clear the changed flag. -// cx_b.update(|cx| { -// collab_ui::init(&client_b.app_state, cx); -// }); -// let project_b = client_b.build_empty_local_project(cx_b); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx)); -// chat_panel_b -// .update(cx_b, |chat_panel, cx| { -// chat_panel.set_active(true, cx); -// chat_panel.select_channel(channel_id, None, cx) -// }) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let b_has_messages = cx_b.read_with(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_new_messages(channel_id) -// .unwrap() -// }); - -// assert!(!b_has_messages); - -// // Sending a message while the chat is open should not change the flag. -// channel_chat_a -// .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let b_has_messages = cx_b.read_with(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_new_messages(channel_id) -// .unwrap() -// }); - -// assert!(!b_has_messages); - -// // Sending a message while the chat is closed should change the flag. -// chat_panel_b.update(cx_b, |chat_panel, cx| { -// chat_panel.set_active(false, cx); -// }); - -// // Sending a message while the chat is open should not change the flag. -// channel_chat_a -// .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let b_has_messages = cx_b.read_with(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_new_messages(channel_id) -// .unwrap() -// }); - -// assert!(b_has_messages); - -// // Closing the chat should re-enable change tracking -// cx_b.update(|_| drop(chat_panel_b)); - -// channel_chat_a -// .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let b_has_messages = cx_b.read_with(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_new_messages(channel_id) -// .unwrap() -// }); - -// assert!(b_has_messages); -// } +#[gpui::test] +async fn test_channel_message_changes( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + // Client A sends a message, client B should see that there is a new message. + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + + executor.run_until_parked(); + + let b_has_messages = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Opening the chat should clear the changed flag. + cx_b.update(|cx| { + collab_ui::init(&client_b.app_state, cx); + }); + let project_b = client_b.build_empty_local_project(cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx)); + chat_panel_b + .update(cx_b, |chat_panel, cx| { + chat_panel.set_active(true, cx); + chat_panel.select_channel(channel_id, None, cx) + }) + .await + .unwrap(); + + executor.run_until_parked(); + + let b_has_messages = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Sending a message while the chat is open should not change the flag. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + executor.run_until_parked(); + + let b_has_messages = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Sending a message while the chat is closed should change the flag. + chat_panel_b.update(cx_b, |chat_panel, cx| { + chat_panel.set_active(false, cx); + }); + + // Sending a message while the chat is open should not change the flag. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + executor.run_until_parked(); + + let b_has_messages = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Closing the chat should re-enable change tracking + cx_b.update(|_| drop(chat_panel_b)); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) + .await + .unwrap(); + + executor.run_until_parked(); + + let b_has_messages = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); +} diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 9209760353935bfdd3573317a9cd3d0ca4e85573..6106f8d5f1531302374de4456bf9fd9aff11b871 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,5 +1,5 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use call::ActiveCall; +use call::{ActiveCall, ParticipantLocation}; use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{ @@ -1568,6 +1568,59 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut }); } +#[gpui::test] +async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let (client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await; + + let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await; + client_a + .host_workspace(&workspace_a, channel_id, cx_a) + .await; + let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await; + + cx_a.simulate_keystrokes("cmd-p 2 enter"); + cx_a.run_until_parked(); + + let editor_a = workspace_a.update(cx_a, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + let editor_b = workspace_b.update(cx_b, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + + // b should follow a to position 1 + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1])) + }); + cx_a.run_until_parked(); + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), vec![1..1]) + }); + + // a unshares the project + cx_a.update(|cx| { + let project = workspace_a.read(cx).project().clone(); + ActiveCall::global(cx).update(cx, |call, cx| { + call.unshare_project(project, cx).unwrap(); + }) + }); + cx_a.run_until_parked(); + + // b should not follow a to position 2 + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([2..2])) + }); + cx_a.run_until_parked(); + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), vec![1..1]) + }); + cx_b.update(|cx| { + let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx); + let participant = room.remote_participants().get(&client_a.id()).unwrap(); + assert_eq!(participant.location, ParticipantLocation::UnsharedProject) + }) +} + #[gpui::test] async fn test_following_into_excluded_file( mut cx_a: &mut TestAppContext, @@ -1593,9 +1646,6 @@ async fn test_following_into_excluded_file( let active_call_b = cx_b.read(ActiveCall::global); let peer_id_a = client_a.peer_id().unwrap(); - cx_a.update(editor::init); - cx_b.update(editor::init); - client_a .fs() .insert_tree( diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2b2bb3a6a42b25e35d1e8763bd58bf6d8c7609fd..4fcf6aa67652fabe9187ba5f78b8899379dc471b 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -113,6 +113,20 @@ impl TestServer { } } + pub async fn start2( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) -> (TestClient, TestClient, u64) { + let mut server = Self::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let channel_id = server + .make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + (client_a, client_b, channel_id) + } + pub async fn reset(&self) { self.app_state.db.reset(); let epoch = self @@ -619,14 +633,49 @@ impl TestClient { "/a", json!({ "1.txt": "one\none\none", - "2.txt": "two\ntwo\ntwo", - "3.txt": "three\nthree\nthree", + "2.js": "function two() { return 2; }", + "3.rs": "mod test", }), ) .await; self.build_local_project("/a", cx).await.0 } + pub async fn host_workspace( + &self, + workspace: &View, + channel_id: u64, + cx: &mut VisualTestContext, + ) { + cx.update(|cx| { + let active_call = ActiveCall::global(cx); + active_call.update(cx, |call, cx| call.join_channel(channel_id, cx)) + }) + .await + .unwrap(); + cx.update(|cx| { + let active_call = ActiveCall::global(cx); + let project = workspace.read(cx).project().clone(); + active_call.update(cx, |call, cx| call.share_project(project, cx)) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + } + + pub async fn join_workspace<'a>( + &'a self, + channel_id: u64, + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) + .await + .unwrap(); + cx.run_until_parked(); + + self.active_workspace(cx) + } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { cx.update(|cx| { Project::local( @@ -670,6 +719,17 @@ impl TestClient { }) } + pub async fn build_test_workspace<'a>( + &'a self, + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + let project = self.build_test_project(cx).await; + cx.add_window_view(|cx| { + cx.activate_window(); + Workspace::new(0, project.clone(), self.app_state.clone(), cx) + }) + } + pub fn active_workspace<'a>( &'a self, cx: &'a mut TestAppContext, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3c0473e67d0a687308c00097c554fc87f47645c1..c8230620b4c7756dbfee2b26011c32634f3b005a 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -111,7 +111,6 @@ fn notification_window_options( let screen_bounds = screen.bounds(); let size: Size = window_size.into(); - // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument. let bounds = gpui::Bounds:: { origin: screen_bounds.upper_right() - point( diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 05aa3816271eef0f1286f7f2fe22367864846888..ce2e5ee3d9eb66c1e4a769dacdb9ff8c357a1ff3 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -11,7 +11,6 @@ use smol::future::yield_now; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; use text::Patch; -use util::ResultExt; pub use super::tab_map::TextSummary; pub type WrapEdit = text::Edit; @@ -154,26 +153,24 @@ impl WrapMap { if let Some(wrap_width) = self.wrap_width { let mut new_snapshot = self.snapshot.clone(); - let mut edits = Patch::default(); + let text_system = cx.text_system().clone(); let (font, font_size) = self.font_with_size.clone(); let task = cx.background_executor().spawn(async move { - if let Some(mut line_wrapper) = text_system.line_wrapper(font, font_size).log_err() - { - let tab_snapshot = new_snapshot.tab_snapshot.clone(); - let range = TabPoint::zero()..tab_snapshot.max_point(); - edits = new_snapshot - .update( - tab_snapshot, - &[TabEdit { - old: range.clone(), - new: range.clone(), - }], - wrap_width, - &mut line_wrapper, - ) - .await; - } + let mut line_wrapper = text_system.line_wrapper(font, font_size); + let tab_snapshot = new_snapshot.tab_snapshot.clone(); + let range = TabPoint::zero()..tab_snapshot.max_point(); + let edits = new_snapshot + .update( + tab_snapshot, + &[TabEdit { + old: range.clone(), + new: range.clone(), + }], + wrap_width, + &mut line_wrapper, + ) + .await; (new_snapshot, edits) }); @@ -245,15 +242,12 @@ impl WrapMap { let (font, font_size) = self.font_with_size.clone(); let update_task = cx.background_executor().spawn(async move { let mut edits = Patch::default(); - if let Some(mut line_wrapper) = - text_system.line_wrapper(font, font_size).log_err() - { - for (tab_snapshot, tab_edits) in pending_edits { - let wrap_edits = snapshot - .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) - .await; - edits = edits.compose(&wrap_edits); - } + let mut line_wrapper = text_system.line_wrapper(font, font_size); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); } (snapshot, edits) }); @@ -1043,7 +1037,7 @@ mod tests { #[gpui::test(iterations = 100)] async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { - // todo!() this test is flaky + // todo this test is flaky init_test(cx); cx.background_executor.set_block_on_ticks(0..=50); @@ -1059,7 +1053,7 @@ mod tests { }; let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let font = font("Helvetica"); - let _font_id = text_system.font_id(&font).unwrap(); + let _font_id = text_system.font_id(&font); let font_size = px(14.0); log::info!("Tab size: {}", tab_size); @@ -1086,7 +1080,7 @@ mod tests { let tabs_snapshot = tab_map.set_max_expansion_column(32); log::info!("TabMap text: {:?}", tabs_snapshot.text()); - let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap(); + let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size); let unwrapped_text = tabs_snapshot.text(); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 66687377bda8d9d217ea6c5acc9b6c5dc6acc186..7fe942f14561c8781b17c9ae66ea356190425422 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7677,7 +7677,6 @@ impl Editor { scrollbar_width: cx.editor_style.scrollbar_width, syntax: cx.editor_style.syntax.clone(), status: cx.editor_style.status.clone(), - // todo!("what about the rest of the highlight style parts for inlays and suggestions?") inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), @@ -8678,6 +8677,10 @@ impl Editor { } } } + + let Some(project) = &self.project else { return }; + let telemetry = project.read(cx).client().telemetry().clone(); + telemetry.log_edit_event("editor"); } multi_buffer::Event::ExcerptsAdded { buffer, @@ -9346,7 +9349,6 @@ impl Render for Editor { scrollbar_width: px(12.), syntax: cx.theme().syntax().clone(), status: cx.theme().status().clone(), - // todo!("what about the rest of the highlight style parts?") inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7efb43bd4852fc7ef1d8e5a5a43a79ad72a0303e..4a648b37709fe62c5e2deef0432ccdd9dbf1d0f4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -809,13 +809,18 @@ impl EditorElement { // the hunk might include the rows of that header. // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. // Instead, we simply check whether the range we're dealing with includes - // any custom elements and if so, we stop painting the diff hunk on the first row of that custom element. + // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header. let end_row_in_current_excerpt = layout .position_map .snapshot .blocks_in_range(start_row..end_row) - .next() - .map(|(start_row, _)| start_row) + .find_map(|(start_row, block)| { + if matches!(block, TransformBlock::ExcerptHeader { .. }) { + Some(start_row) + } else { + None + } + }) .unwrap_or(end_row); let start_y = start_row as f32 * line_height - scroll_top; @@ -878,16 +883,23 @@ impl EditorElement { let fold_corner_radius = 0.15 * layout.position_map.line_height; cx.with_element_id(Some("folds"), |cx| { let snapshot = &layout.position_map.snapshot; + for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) { let fold_range = fold.range.clone(); let display_range = fold.range.start.to_display_point(&snapshot) ..fold.range.end.to_display_point(&snapshot); debug_assert_eq!(display_range.start.row(), display_range.end.row()); let row = display_range.start.row(); + debug_assert!(row < layout.visible_display_row_range.end); + let Some(line_layout) = &layout + .position_map + .line_layouts + .get((row - layout.visible_display_row_range.start) as usize) + .map(|l| &l.line) + else { + continue; + }; - let line_layout = &layout.position_map.line_layouts - [(row - layout.visible_display_row_range.start) as usize] - .line; let start_x = content_origin.x + line_layout.x_for_index(display_range.start.column() as usize) - layout.position_map.scroll_position.x; @@ -1010,7 +1022,6 @@ impl EditorElement { .chars_at(cursor_position) .next() .and_then(|(character, _)| { - // todo!() currently shape_line panics if text conatins newlines let text = if character == '\n' { SharedString::from(" ") } else { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 26ce3e5cf708e0193eef2c87fbf72c27f4da806a..8da2f50c198a01136d1318922a5159e3814a894f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -2,13 +2,13 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, - ExcerptId, RangeToAnchorExt, + ExcerptId, Hover, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ - actions, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, - Task, ViewContext, WeakView, + div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, MouseButton, + ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task, + ViewContext, WeakView, }; use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; @@ -27,8 +27,6 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); pub const HOVER_POPOVER_GAP: Pixels = px(10.); -actions!(editor, [Hover]); - /// Bindable action which uses the most recent selection head to trigger a hover pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { let head = editor.selections.newest_display(cx).head(); diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index bf7a0715604f20ec6eae1d28ea4988016a8cd2cf..2444a8e94850f8128aafb0948cb356eee81f1086 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -525,43 +525,4 @@ impl Render for FeedbackModal { } } -// TODO: Testing of various button states, dismissal prompts, etc. - -// #[cfg(test)] -// mod test { -// use super::*; - -// #[test] -// fn test_invalid_email_addresses() { -// let markdown = markdown.await.log_err(); -// let buffer = project.update(&mut cx, |project, cx| { -// project.create_buffer("", markdown, cx) -// })??; - -// workspace.update(&mut cx, |workspace, cx| { -// let system_specs = SystemSpecs::new(cx); - -// workspace.toggle_modal(cx, move |cx| { -// let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx); - -// assert!(!feedback_modal.can_submit()); -// assert!(!feedback_modal.valid_email_address(cx)); -// assert!(!feedback_modal.valid_character_count()); - -// feedback_modal -// .email_address_editor -// .update(cx, |this, cx| this.set_text("a", cx)); -// feedback_modal.set_submission_state(cx); - -// assert!(!feedback_modal.valid_email_address(cx)); - -// feedback_modal -// .email_address_editor -// .update(cx, |this, cx| this.set_text("a&b.com", cx)); -// feedback_modal.set_submission_state(cx); - -// assert!(feedback_modal.valid_email_address(cx)); -// }); -// })?; -// } -// } +// TODO: Testing of various button states, dismissal prompts, etc. :) diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 269d5790bc666fa12c7ef9a31d91356458b36db9..e80e310c707895352184ba903e146359922dd831 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -23,6 +23,7 @@ theme = { path = "../theme" } ui = { path = "../ui" } workspace = { path = "../workspace" } postage.workspace = true +anyhow.workspace = true serde.workspace = true [dev-dependencies] diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index d49eb9ee603d7e819bab5097adac874107fd9a3f..ed3e57ba28a0bfbebbcac1f8aff3482c953be276 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -312,7 +312,7 @@ impl FileFinderDelegate { cx: &mut ViewContext, ) -> Self { cx.observe(&project, |file_finder, _, cx| { - //todo!() We should probably not re-render on every project anything + //todo We should probably not re-render on every project anything file_finder .picker .update(cx, |picker, cx| picker.refresh(cx)) @@ -519,6 +519,62 @@ impl FileFinderDelegate { (file_name, file_name_positions, full_path, path_positions) } + + fn lookup_absolute_path( + &self, + query: PathLikeWithPosition, + cx: &mut ViewContext<'_, Picker>, + ) -> Task<()> { + cx.spawn(|picker, mut cx| async move { + let Some((project, fs)) = picker + .update(&mut cx, |picker, cx| { + let fs = Arc::clone(&picker.delegate.project.read(cx).fs()); + (picker.delegate.project.clone(), fs) + }) + .log_err() + else { + return; + }; + + let query_path = Path::new(query.path_like.path_query()); + let mut path_matches = Vec::new(); + match fs.metadata(query_path).await.log_err() { + Some(Some(_metadata)) => { + let update_result = project + .update(&mut cx, |project, cx| { + if let Some((worktree, relative_path)) = + project.find_local_worktree(query_path, cx) + { + path_matches.push(PathMatch { + score: 0.0, + positions: Vec::new(), + worktree_id: worktree.read(cx).id().to_usize(), + path: Arc::from(relative_path), + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }); + } + }) + .log_err(); + if update_result.is_none() { + return; + } + } + Some(None) => {} + None => return, + } + + picker + .update(&mut cx, |picker, cx| { + let picker_delegate = &mut picker.delegate; + let search_id = util::post_inc(&mut picker_delegate.search_count); + picker_delegate.set_search_matches(search_id, false, query, path_matches, cx); + + anyhow::Ok(()) + }) + .log_err(); + }) + } } impl PickerDelegate for FileFinderDelegate { @@ -588,7 +644,12 @@ impl PickerDelegate for FileFinderDelegate { }) }) .expect("infallible"); - self.spawn_search(query, cx) + + if Path::new(query.path_like.path_query()).is_absolute() { + self.lookup_absolute_path(query, cx) + } else { + self.spawn_search(query, cx) + } } } @@ -818,6 +879,68 @@ mod tests { } } + #[gpui::test] + async fn test_absolute_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1.txt": "", + "b": { + "file2.txt": "", + }, + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, workspace, cx) = build_find_picker(project, cx); + + let matching_abs_path = "/root/a/b/file2.txt"; + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(matching_abs_path.to_string(), cx) + }) + .await; + picker.update(cx, |picker, _| { + assert_eq!( + collect_search_results(picker), + vec![PathBuf::from("a/b/file2.txt")], + "Matching abs path should be the only match" + ) + }); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!(active_editor.read(cx).title(cx), "file2.txt"); + }); + + let mismatching_abs_path = "/root/a/b/file1.txt"; + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(mismatching_abs_path.to_string(), cx) + }) + .await; + picker.update(cx, |picker, _| { + assert_eq!( + collect_search_results(picker), + Vec::::new(), + "Mismatching abs path should produce no matches" + ) + }); + } + #[gpui::test] async fn test_complex_path(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index ef02316f83ea9dfa57c68106f6b5706b755e3cd6..9caa0da4823f64f5c30c32619a7b6950b305e676 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -170,7 +170,7 @@ impl ActionRegistry { macro_rules! actions { ($namespace:path, [ $($name:ident),* $(,)? ]) => { $( - #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)] + #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)] #[serde(crate = "gpui::private::serde")] pub struct $name; diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index ab42f1278c3a837c2895d77fdf388a842ed2433b..5410ddce06e9ca999aaee0911fd7a69d6a0e61c8 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -28,11 +28,11 @@ impl KeystrokeMatcher { /// Pushes a keystroke onto the matcher. /// The result of the new keystroke is returned: - /// KeyMatch::None => + /// - KeyMatch::None => /// No match is valid for this key given any pending keystrokes. - /// KeyMatch::Pending => + /// - KeyMatch::Pending => /// There exist bindings which are still waiting for more keys. - /// KeyMatch::Complete(matches) => + /// - KeyMatch::Complete(matches) => /// One or more bindings have received the necessary key presses. /// Bindings added later will take precedence over earlier bindings. pub fn match_keystroke( @@ -77,12 +77,10 @@ impl KeystrokeMatcher { if let Some(pending_key) = pending_key { self.pending_keystrokes.push(pending_key); - } - - if self.pending_keystrokes.is_empty() { - KeyMatch::None - } else { KeyMatch::Pending + } else { + self.pending_keystrokes.clear(); + KeyMatch::None } } } @@ -98,367 +96,374 @@ impl KeyMatch { pub fn is_some(&self) -> bool { matches!(self, KeyMatch::Some(_)) } + + pub fn matches(self) -> Option>> { + match self { + KeyMatch::Some(matches) => Some(matches), + _ => None, + } + } +} + +impl PartialEq for KeyMatch { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (KeyMatch::None, KeyMatch::None) => true, + (KeyMatch::Pending, KeyMatch::Pending) => true, + (KeyMatch::Some(a), KeyMatch::Some(b)) => { + if a.len() != b.len() { + return false; + } + + for (a, b) in a.iter().zip(b.iter()) { + if !a.partial_eq(b.as_ref()) { + return false; + } + } + + true + } + _ => false, + } + } } -// #[cfg(test)] -// mod tests { -// use anyhow::Result; -// use serde::Deserialize; - -// use crate::{actions, impl_actions, keymap_matcher::ActionContext}; - -// use super::*; - -// #[test] -// fn test_keymap_and_view_ordering() -> Result<()> { -// actions!(test, [EditorAction, ProjectPanelAction]); - -// let mut editor = ActionContext::default(); -// editor.add_identifier("Editor"); - -// let mut project_panel = ActionContext::default(); -// project_panel.add_identifier("ProjectPanel"); - -// // Editor 'deeper' in than project panel -// let dispatch_path = vec![(2, editor), (1, project_panel)]; - -// // But editor actions 'higher' up in keymap -// let keymap = Keymap::new(vec![ -// Binding::new("left", EditorAction, Some("Editor")), -// Binding::new("left", ProjectPanelAction, Some("ProjectPanel")), -// ]); - -// let mut matcher = KeymapMatcher::new(keymap); - -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()), -// KeyMatch::Matches(vec![ -// (2, Box::new(EditorAction)), -// (1, Box::new(ProjectPanelAction)), -// ]), -// ); - -// Ok(()) -// } - -// #[test] -// fn test_push_keystroke() -> Result<()> { -// actions!(test, [B, AB, C, D, DA, E, EF]); - -// let mut context1 = ActionContext::default(); -// context1.add_identifier("1"); - -// let mut context2 = ActionContext::default(); -// context2.add_identifier("2"); - -// let dispatch_path = vec![(2, context2), (1, context1)]; - -// let keymap = Keymap::new(vec![ -// Binding::new("a b", AB, Some("1")), -// Binding::new("b", B, Some("2")), -// Binding::new("c", C, Some("2")), -// Binding::new("d", D, Some("1")), -// Binding::new("d", D, Some("2")), -// Binding::new("d a", DA, Some("2")), -// ]); - -// let mut matcher = KeymapMatcher::new(keymap); - -// // Binding with pending prefix always takes precedence -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), -// KeyMatch::Pending, -// ); -// // B alone doesn't match because a was pending, so AB is returned instead -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), -// KeyMatch::Matches(vec![(1, Box::new(AB))]), -// ); -// assert!(!matcher.has_pending_keystrokes()); - -// // Without an a prefix, B is dispatched like expected -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), -// KeyMatch::Matches(vec![(2, Box::new(B))]), -// ); -// assert!(!matcher.has_pending_keystrokes()); - -// // If a is prefixed, C will not be dispatched because there -// // was a pending binding for it -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), -// KeyMatch::Pending, -// ); -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()), -// KeyMatch::None, -// ); -// assert!(!matcher.has_pending_keystrokes()); - -// // If a single keystroke matches multiple bindings in the tree -// // all of them are returned so that we can fallback if the action -// // handler decides to propagate the action -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()), -// KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]), -// ); - -// // If none of the d action handlers consume the binding, a pending -// // binding may then be used -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), -// KeyMatch::Matches(vec![(2, Box::new(DA))]), -// ); -// assert!(!matcher.has_pending_keystrokes()); - -// Ok(()) -// } - -// #[test] -// fn test_keystroke_parsing() -> Result<()> { -// assert_eq!( -// Keystroke::parse("ctrl-p")?, -// Keystroke { -// key: "p".into(), -// ctrl: true, -// alt: false, -// shift: false, -// cmd: false, -// function: false, -// ime_key: None, -// } -// ); - -// assert_eq!( -// Keystroke::parse("alt-shift-down")?, -// Keystroke { -// key: "down".into(), -// ctrl: false, -// alt: true, -// shift: true, -// cmd: false, -// function: false, -// ime_key: None, -// } -// ); - -// assert_eq!( -// Keystroke::parse("shift-cmd--")?, -// Keystroke { -// key: "-".into(), -// ctrl: false, -// alt: false, -// shift: true, -// cmd: true, -// function: false, -// ime_key: None, -// } -// ); - -// Ok(()) -// } - -// #[test] -// fn test_context_predicate_parsing() -> Result<()> { -// use KeymapContextPredicate::*; - -// assert_eq!( -// KeymapContextPredicate::parse("a && (b == c || d != e)")?, -// And( -// Box::new(Identifier("a".into())), -// Box::new(Or( -// Box::new(Equal("b".into(), "c".into())), -// Box::new(NotEqual("d".into(), "e".into())), -// )) -// ) -// ); - -// assert_eq!( -// KeymapContextPredicate::parse("!a")?, -// Not(Box::new(Identifier("a".into())),) -// ); - -// Ok(()) -// } - -// #[test] -// fn test_context_predicate_eval() { -// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap(); - -// let mut context = ActionContext::default(); -// context.add_identifier("a"); -// assert!(!predicate.eval(&[context])); - -// let mut context = ActionContext::default(); -// context.add_identifier("a"); -// context.add_identifier("b"); -// assert!(predicate.eval(&[context])); - -// let mut context = ActionContext::default(); -// context.add_identifier("a"); -// context.add_key("c", "x"); -// assert!(!predicate.eval(&[context])); - -// let mut context = ActionContext::default(); -// context.add_identifier("a"); -// context.add_key("c", "d"); -// assert!(predicate.eval(&[context])); - -// let predicate = KeymapContextPredicate::parse("!a").unwrap(); -// assert!(predicate.eval(&[ActionContext::default()])); -// } - -// #[test] -// fn test_context_child_predicate_eval() { -// let predicate = KeymapContextPredicate::parse("a && b > c").unwrap(); -// let contexts = [ -// context_set(&["e", "f"]), -// context_set(&["c", "d"]), // match this context -// context_set(&["a", "b"]), -// ]; - -// assert!(!predicate.eval(&contexts[0..])); -// assert!(predicate.eval(&contexts[1..])); -// assert!(!predicate.eval(&contexts[2..])); - -// let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap(); -// let contexts = [ -// context_set(&["f"]), -// context_set(&["e"]), // only match this context -// context_set(&["c"]), -// context_set(&["a", "b"]), -// context_set(&["e"]), -// context_set(&["c", "d"]), -// context_set(&["a", "b"]), -// ]; - -// assert!(!predicate.eval(&contexts[0..])); -// assert!(predicate.eval(&contexts[1..])); -// assert!(!predicate.eval(&contexts[2..])); -// assert!(!predicate.eval(&contexts[3..])); -// assert!(!predicate.eval(&contexts[4..])); -// assert!(!predicate.eval(&contexts[5..])); -// assert!(!predicate.eval(&contexts[6..])); - -// fn context_set(names: &[&str]) -> ActionContext { -// let mut keymap = ActionContext::new(); -// names -// .iter() -// .for_each(|name| keymap.add_identifier(name.to_string())); -// keymap -// } -// } - -// #[test] -// fn test_matcher() -> Result<()> { -// #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] -// pub struct A(pub String); -// impl_actions!(test, [A]); -// actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]); - -// #[derive(Clone, Debug, Eq, PartialEq)] -// struct ActionArg { -// a: &'static str, -// } - -// let keymap = Keymap::new(vec![ -// Binding::new("a", A("x".to_string()), Some("a")), -// Binding::new("b", B, Some("a")), -// Binding::new("a b", Ab, Some("a || b")), -// Binding::new("$", Dollar, Some("a")), -// Binding::new("\"", Quote, Some("a")), -// Binding::new("alt-s", Ess, Some("a")), -// Binding::new("ctrl-`", Backtick, Some("a")), -// ]); - -// let mut context_a = ActionContext::default(); -// context_a.add_identifier("a"); - -// let mut context_b = ActionContext::default(); -// context_b.add_identifier("b"); - -// let mut matcher = KeymapMatcher::new(keymap); - -// // Basic match -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))]) -// ); -// matcher.clear_pending(); - -// // Multi-keystroke match -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), -// KeyMatch::Pending -// ); -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Ab))]) -// ); -// matcher.clear_pending(); - -// // Failed matches don't interfere with matching subsequent keys -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]), -// KeyMatch::None -// ); -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))]) -// ); -// matcher.clear_pending(); - -// // Pending keystrokes are cleared when the context changes -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), -// KeyMatch::Pending -// ); -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]), -// KeyMatch::None -// ); -// matcher.clear_pending(); - -// let mut context_c = ActionContext::default(); -// context_c.add_identifier("c"); - -// // Pending keystrokes are maintained per-view -// assert_eq!( -// matcher.match_keystroke( -// Keystroke::parse("a")?, -// vec![(1, context_b.clone()), (2, context_c.clone())] -// ), -// KeyMatch::Pending -// ); -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Ab))]) -// ); - -// // handle Czech $ (option + 4 key) -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Dollar))]) -// ); - -// // handle Brazillian quote (quote key then space key) -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Quote))]) -// ); - -// // handle ctrl+` on a brazillian keyboard -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Backtick))]) -// ); - -// // handle alt-s on a US keyboard -// assert_eq!( -// matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]), -// KeyMatch::Matches(vec![(1, Box::new(Ess))]) -// ); - -// Ok(()) -// } -// } +#[cfg(test)] +mod tests { + + use serde_derive::Deserialize; + + use super::*; + use crate::{self as gpui, KeyBindingContextPredicate, Modifiers}; + use crate::{actions, KeyBinding}; + + #[test] + fn test_keymap_and_view_ordering() { + actions!(test, [EditorAction, ProjectPanelAction]); + + let mut editor = KeyContext::default(); + editor.add("Editor"); + + let mut project_panel = KeyContext::default(); + project_panel.add("ProjectPanel"); + + // Editor 'deeper' in than project panel + let dispatch_path = vec![project_panel, editor]; + + // But editor actions 'higher' up in keymap + let keymap = Keymap::new(vec![ + KeyBinding::new("left", EditorAction, Some("Editor")), + KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")), + ]); + + let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); + + let matches = matcher + .match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path) + .matches() + .unwrap(); + + assert!(matches[0].partial_eq(&EditorAction)); + assert!(matches.get(1).is_none()); + } + + #[test] + fn test_multi_keystroke_match() { + actions!(test, [B, AB, C, D, DA, E, EF]); + + let mut context1 = KeyContext::default(); + context1.add("1"); + + let mut context2 = KeyContext::default(); + context2.add("2"); + + let dispatch_path = vec![context2, context1]; + + let keymap = Keymap::new(vec![ + KeyBinding::new("a b", AB, Some("1")), + KeyBinding::new("b", B, Some("2")), + KeyBinding::new("c", C, Some("2")), + KeyBinding::new("d", D, Some("1")), + KeyBinding::new("d", D, Some("2")), + KeyBinding::new("d a", DA, Some("2")), + ]); + + let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); + + // Binding with pending prefix always takes precedence + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path), + KeyMatch::Pending, + ); + // B alone doesn't match because a was pending, so AB is returned instead + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path), + KeyMatch::Some(vec![Box::new(AB)]), + ); + assert!(!matcher.has_pending_keystrokes()); + + // Without an a prefix, B is dispatched like expected + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]), + KeyMatch::Some(vec![Box::new(B)]), + ); + assert!(!matcher.has_pending_keystrokes()); + + eprintln!("PROBLEM AREA"); + // If a is prefixed, C will not be dispatched because there + // was a pending binding for it + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path), + KeyMatch::Pending, + ); + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path), + KeyMatch::None, + ); + assert!(!matcher.has_pending_keystrokes()); + + // If a single keystroke matches multiple bindings in the tree + // only one of them is returned. + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path), + KeyMatch::Some(vec![Box::new(D)]), + ); + } + + #[test] + fn test_keystroke_parsing() { + assert_eq!( + Keystroke::parse("ctrl-p").unwrap(), + Keystroke { + key: "p".into(), + modifiers: Modifiers { + control: true, + alt: false, + shift: false, + command: false, + function: false, + }, + ime_key: None, + } + ); + + assert_eq!( + Keystroke::parse("alt-shift-down").unwrap(), + Keystroke { + key: "down".into(), + modifiers: Modifiers { + control: false, + alt: true, + shift: true, + command: false, + function: false, + }, + ime_key: None, + } + ); + + assert_eq!( + Keystroke::parse("shift-cmd--").unwrap(), + Keystroke { + key: "-".into(), + modifiers: Modifiers { + control: false, + alt: false, + shift: true, + command: true, + function: false, + }, + ime_key: None, + } + ); + } + + #[test] + fn test_context_predicate_parsing() { + use KeyBindingContextPredicate::*; + + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )) + ) + ); + + assert_eq!( + KeyBindingContextPredicate::parse("!a").unwrap(), + Not(Box::new(Identifier("a".into())),) + ); + } + + #[test] + fn test_context_predicate_eval() { + let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap(); + + let mut context = KeyContext::default(); + context.add("a"); + assert!(!predicate.eval(&[context])); + + let mut context = KeyContext::default(); + context.add("a"); + context.add("b"); + assert!(predicate.eval(&[context])); + + let mut context = KeyContext::default(); + context.add("a"); + context.set("c", "x"); + assert!(!predicate.eval(&[context])); + + let mut context = KeyContext::default(); + context.add("a"); + context.set("c", "d"); + assert!(predicate.eval(&[context])); + + let predicate = KeyBindingContextPredicate::parse("!a").unwrap(); + assert!(predicate.eval(&[KeyContext::default()])); + } + + #[test] + fn test_context_child_predicate_eval() { + let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap(); + let contexts = [ + context_set(&["a", "b"]), + context_set(&["c", "d"]), // match this context + context_set(&["e", "f"]), + ]; + + assert!(!predicate.eval(&contexts[..=0])); + assert!(predicate.eval(&contexts[..=1])); + assert!(!predicate.eval(&contexts[..=2])); + + let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap(); + let contexts = [ + context_set(&["a", "b"]), + context_set(&["c", "d"]), + context_set(&["e"]), + context_set(&["a", "b"]), + context_set(&["c"]), + context_set(&["e"]), // only match this context + context_set(&["f"]), + ]; + + assert!(!predicate.eval(&contexts[..=0])); + assert!(!predicate.eval(&contexts[..=1])); + assert!(!predicate.eval(&contexts[..=2])); + assert!(!predicate.eval(&contexts[..=3])); + assert!(!predicate.eval(&contexts[..=4])); + assert!(predicate.eval(&contexts[..=5])); + assert!(!predicate.eval(&contexts[..=6])); + + fn context_set(names: &[&str]) -> KeyContext { + let mut keymap = KeyContext::default(); + names.iter().for_each(|name| keymap.add(name.to_string())); + keymap + } + } + + #[test] + fn test_matcher() { + #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] + pub struct A(pub String); + impl_actions!(test, [A]); + actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]); + + #[derive(Clone, Debug, Eq, PartialEq)] + struct ActionArg { + a: &'static str, + } + + let keymap = Keymap::new(vec![ + KeyBinding::new("a", A("x".to_string()), Some("a")), + KeyBinding::new("b", B, Some("a")), + KeyBinding::new("a b", Ab, Some("a || b")), + KeyBinding::new("$", Dollar, Some("a")), + KeyBinding::new("\"", Quote, Some("a")), + KeyBinding::new("alt-s", Ess, Some("a")), + KeyBinding::new("ctrl-`", Backtick, Some("a")), + ]); + + let mut context_a = KeyContext::default(); + context_a.add("a"); + + let mut context_b = KeyContext::default(); + context_b.add("b"); + + let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); + + // Basic match + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]), + KeyMatch::Some(vec![Box::new(A("x".to_string()))]) + ); + matcher.clear_pending(); + + // Multi-keystroke match + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]), + KeyMatch::Pending + ); + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]), + KeyMatch::Some(vec![Box::new(Ab)]) + ); + matcher.clear_pending(); + + // Failed matches don't interfere with matching subsequent keys + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]), + KeyMatch::None + ); + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]), + KeyMatch::Some(vec![Box::new(A("x".to_string()))]) + ); + matcher.clear_pending(); + + let mut context_c = KeyContext::default(); + context_c.add("c"); + + assert_eq!( + matcher.match_keystroke( + &Keystroke::parse("a").unwrap(), + &[context_c.clone(), context_b.clone()] + ), + KeyMatch::Pending + ); + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]), + KeyMatch::Some(vec![Box::new(Ab)]) + ); + + // handle Czech $ (option + 4 key) + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]), + KeyMatch::Some(vec![Box::new(Dollar)]) + ); + + // handle Brazillian quote (quote key then space key) + assert_eq!( + matcher.match_keystroke( + &Keystroke::parse("space->\"").unwrap(), + &[context_a.clone()] + ), + KeyMatch::Some(vec![Box::new(Quote)]) + ); + + // handle ctrl+` on a brazillian keyboard + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]), + KeyMatch::Some(vec![Box::new(Backtick)]) + ); + + // handle alt-s on a US keyboard + assert_eq!( + matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]), + KeyMatch::Some(vec![Box::new(Ess)]) + ); + } +} diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index d9f7936066b248a7037fb2ea810b7c4a5dc431d2..79ffb8dc8e4aa54b954494a8cae4f3ca6186b377 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -592,169 +592,49 @@ impl From for FontkitStyle { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::AppContext; -// use font_kit::properties::{Style, Weight}; -// use platform::FontSystem as _; - -// #[crate::test(self, retries = 5)] -// fn test_layout_str(_: &mut AppContext) { -// // This is failing intermittently on CI and we don't have time to figure it out -// let fonts = FontSystem::new(); -// let menlo = fonts.load_family("Menlo", &Default::default()).unwrap(); -// let menlo_regular = RunStyle { -// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(), -// color: Default::default(), -// underline: Default::default(), -// }; -// let menlo_italic = RunStyle { -// font_id: fonts -// .select_font(&menlo, Properties::new().style(Style::Italic)) -// .unwrap(), -// color: Default::default(), -// underline: Default::default(), -// }; -// let menlo_bold = RunStyle { -// font_id: fonts -// .select_font(&menlo, Properties::new().weight(Weight::BOLD)) -// .unwrap(), -// color: Default::default(), -// underline: Default::default(), -// }; -// assert_ne!(menlo_regular, menlo_italic); -// assert_ne!(menlo_regular, menlo_bold); -// assert_ne!(menlo_italic, menlo_bold); - -// let line = fonts.layout_line( -// "hello world", -// 16.0, -// &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)], -// ); -// assert_eq!(line.runs.len(), 3); -// assert_eq!(line.runs[0].font_id, menlo_bold.font_id); -// assert_eq!(line.runs[0].glyphs.len(), 2); -// assert_eq!(line.runs[1].font_id, menlo_italic.font_id); -// assert_eq!(line.runs[1].glyphs.len(), 4); -// assert_eq!(line.runs[2].font_id, menlo_regular.font_id); -// assert_eq!(line.runs[2].glyphs.len(), 5); -// } - -// #[test] -// fn test_glyph_offsets() -> crate::Result<()> { -// let fonts = FontSystem::new(); -// let zapfino = fonts.load_family("Zapfino", &Default::default())?; -// let zapfino_regular = RunStyle { -// font_id: fonts.select_font(&zapfino, &Properties::new())?, -// color: Default::default(), -// underline: Default::default(), -// }; -// let menlo = fonts.load_family("Menlo", &Default::default())?; -// let menlo_regular = RunStyle { -// font_id: fonts.select_font(&menlo, &Properties::new())?, -// color: Default::default(), -// underline: Default::default(), -// }; - -// let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; -// let line = fonts.layout_line( -// text, -// 16.0, -// &[ -// (9, zapfino_regular), -// (13, menlo_regular), -// (text.len() - 22, zapfino_regular), -// ], -// ); -// assert_eq!( -// line.runs -// .iter() -// .flat_map(|r| r.glyphs.iter()) -// .map(|g| g.index) -// .collect::>(), -// vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37], -// ); -// Ok(()) -// } - -// #[test] -// #[ignore] -// fn test_rasterize_glyph() { -// use std::{fs::File, io::BufWriter, path::Path}; - -// let fonts = FontSystem::new(); -// let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap(); -// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); -// let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap(); - -// const VARIANTS: usize = 1; -// for i in 0..VARIANTS { -// let variant = i as f32 / VARIANTS as f32; -// let (bounds, bytes) = fonts -// .rasterize_glyph( -// font_id, -// 16.0, -// glyph_id, -// vec2f(variant, variant), -// 2., -// RasterizationOptions::Alpha, -// ) -// .unwrap(); - -// let name = format!("/Users/as-cii/Desktop/twog-{}.png", i); -// let path = Path::new(&name); -// let file = File::create(path).unwrap(); -// let w = &mut BufWriter::new(file); - -// let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32); -// encoder.set_color(png::ColorType::Grayscale); -// encoder.set_depth(png::BitDepth::Eight); -// let mut writer = encoder.write_header().unwrap(); -// writer.write_image_data(&bytes).unwrap(); -// } -// } - -// #[test] -// fn test_wrap_line() { -// let fonts = FontSystem::new(); -// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap(); -// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); - -// let line = "one two three four five\n"; -// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0); -// assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]); - -// let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n"; -// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0); -// assert_eq!( -// wrap_boundaries, -// &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),] -// ); -// } - -// #[test] -// fn test_layout_line_bom_char() { -// let fonts = FontSystem::new(); -// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap(); -// let style = RunStyle { -// font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(), -// color: Default::default(), -// underline: Default::default(), -// }; - -// let line = "\u{feff}"; -// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); -// assert_eq!(layout.len, line.len()); -// assert!(layout.runs.is_empty()); - -// let line = "a\u{feff}b"; -// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); -// assert_eq!(layout.len, line.len()); -// assert_eq!(layout.runs.len(), 1); -// assert_eq!(layout.runs[0].glyphs.len(), 2); -// assert_eq!(layout.runs[0].glyphs[0].id, 68); // a -// // There's no glyph for \u{feff} -// assert_eq!(layout.runs[0].glyphs[1].id, 69); // b -// } -// } +#[cfg(test)] +mod tests { + use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem}; + + #[test] + fn test_wrap_line() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + + let line = "one two three four five\n"; + let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0)); + assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]); + + let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n"; + let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0)); + assert_eq!( + wrap_boundaries, + &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),] + ); + } + + #[test] + fn test_layout_line_bom_char() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + let line = "\u{feff}"; + let mut style = FontRun { + font_id, + len: line.len(), + }; + + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert!(layout.runs.is_empty()); + + let line = "a\u{feff}b"; + style.len = line.len(); + let layout = fonts.layout_line(line, px(16.), &[style]); + assert_eq!(layout.len, line.len()); + assert_eq!(layout.runs.len(), 1); + assert_eq!(layout.runs[0].glyphs.len(), 2); + assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a + // There's no glyph for \u{feff} + assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b + } +} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 6d03a3b5cd698c62147bd75b62c3594309f38797..6d4fd9c4896060b30621804ce532e97e0d369138 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -269,6 +269,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C sel!(windowShouldClose:), window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, ); + decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); decl.add_method( @@ -685,9 +686,6 @@ impl Drop for MacWindow { this.executor .spawn(async move { unsafe { - // todo!() this panic()s when you click the red close button - // unless should_close returns false. - // (luckliy in zed it always returns false) window.close(); } }) @@ -1097,37 +1095,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: .flatten() .is_some(); if !is_composing { - // if the IME has changed the key, we'll first emit an event with the character - // generated by the IME system; then fallback to the keystroke if that is not - // handled. - // cases that we have working: - // - " on a brazillian layout by typing - // - ctrl-` on a brazillian layout by typing - // - $ on a czech QWERTY layout by typing - // - 4 on a czech QWERTY layout by typing - // - ctrl-4 on a czech QWERTY layout by typing (or ) - if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) { - let event_with_ime_text = KeyDownEvent { - is_held: false, - keystroke: Keystroke { - // we match ctrl because some use-cases need it. - // we don't match alt because it's often used to generate the optional character - // we don't match shift because we're not here with letters (usually) - // we don't match cmd/fn because they don't seem to use IME - modifiers: Default::default(), - key: ime_text.clone().unwrap(), - ime_key: None, // todo!("handle IME key") - }, - }; - handled = callback(InputEvent::KeyDown(event_with_ime_text)); - } - if !handled { - // empty key happens when you type a deadkey in input composition. - // (e.g. on a brazillian keyboard typing quote is a deadkey) - if !event.keystroke.key.is_empty() { - handled = callback(InputEvent::KeyDown(event)); - } - } + handled = callback(InputEvent::KeyDown(event)); } if !handled { @@ -1570,6 +1538,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS replacement_range, text: text.to_string(), }); + if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key { + pending_key_down.0.keystroke.ime_key = Some(text.to_string()); + } window_state.lock().pending_key_down = Some(pending_key_down); } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 47073bcde0ef77b7b6674d0c0a24b7dfa107e948..2d3cc34f3fc09e6402ece055f9aa6ba45fe5434e 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -364,28 +364,20 @@ impl TextSystem { self.line_layout_cache.start_frame() } - pub fn line_wrapper( - self: &Arc, - font: Font, - font_size: Pixels, - ) -> Result { + pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { let lock = &mut self.wrapper_pool.lock(); - let font_id = self.font_id(&font)?; + let font_id = self.resolve_font(&font); let wrappers = lock .entry(FontIdWithSize { font_id, font_size }) .or_default(); - let wrapper = wrappers.pop().map(anyhow::Ok).unwrap_or_else(|| { - Ok(LineWrapper::new( - font_id, - font_size, - self.platform_text_system.clone(), - )) - })?; + let wrapper = wrappers.pop().unwrap_or_else(|| { + LineWrapper::new(font_id, font_size, self.platform_text_system.clone()) + }); - Ok(LineWrapperHandle { + LineWrapperHandle { wrapper: Some(wrapper), text_system: self.clone(), - }) + } } pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 79013adbb2c83cd0e8e816ca5ef7bb88618b5ae8..f6963dbfd4ed6dec3c467741a45d6c5531aa0788 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -137,7 +137,7 @@ impl Boundary { #[cfg(test)] mod tests { use super::*; - use crate::{font, TestAppContext, TestDispatcher}; + use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary}; use rand::prelude::*; #[test] @@ -206,75 +206,70 @@ mod tests { }); } - // todo!("move this to a test on TextSystem::layout_text") - // todo! repeat this test - // #[test] - // fn test_wrap_shaped_line() { - // App::test().run(|cx| { - // let text_system = cx.text_system().clone(); + // For compatibility with the test macro + use crate as gpui; - // let normal = TextRun { - // len: 0, - // font: font("Helvetica"), - // color: Default::default(), - // underline: Default::default(), - // }; - // let bold = TextRun { - // len: 0, - // font: font("Helvetica").bold(), - // color: Default::default(), - // underline: Default::default(), - // }; + #[crate::test] + fn test_wrap_shaped_line(cx: &mut TestAppContext) { + cx.update(|cx| { + let text_system = cx.text_system().clone(); + + let normal = TextRun { + len: 0, + font: font("Helvetica"), + color: Default::default(), + underline: Default::default(), + background_color: None, + }; + let bold = TextRun { + len: 0, + font: font("Helvetica").bold(), + color: Default::default(), + underline: Default::default(), + background_color: None, + }; - // impl TextRun { - // fn with_len(&self, len: usize) -> Self { - // let mut this = self.clone(); - // this.len = len; - // this - // } - // } + impl TextRun { + fn with_len(&self, len: usize) -> Self { + let mut this = self.clone(); + this.len = len; + this + } + } - // let text = "aa bbb cccc ddddd eeee".into(); - // let lines = text_system - // .layout_text( - // &text, - // px(16.), - // &[ - // normal.with_len(4), - // bold.with_len(5), - // normal.with_len(6), - // bold.with_len(1), - // normal.with_len(7), - // ], - // None, - // ) - // .unwrap(); - // let line = &lines[0]; + let text = "aa bbb cccc ddddd eeee".into(); + let lines = text_system + .shape_text( + text, + px(16.), + &[ + normal.with_len(4), + bold.with_len(5), + normal.with_len(6), + bold.with_len(1), + normal.with_len(7), + ], + Some(px(72.)), + ) + .unwrap(); - // let mut wrapper = LineWrapper::new( - // text_system.font_id(&normal.font).unwrap(), - // px(16.), - // text_system.platform_text_system.clone(), - // ); - // assert_eq!( - // wrapper - // .wrap_shaped_line(&text, &line, px(72.)) - // .collect::>(), - // &[ - // ShapedBoundary { - // run_ix: 1, - // glyph_ix: 3 - // }, - // ShapedBoundary { - // run_ix: 2, - // glyph_ix: 3 - // }, - // ShapedBoundary { - // run_ix: 4, - // glyph_ix: 2 - // } - // ], - // ); - // }); - // } + assert_eq!( + lines[0].layout.wrap_boundaries(), + &[ + WrapBoundary { + run_ix: 1, + glyph_ix: 3 + }, + WrapBoundary { + run_ix: 2, + glyph_ix: 3 + }, + WrapBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + }); + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 187f28c14b460c08e20500bed19538810f5d814a..509a6d8466609b5041f4f958ca1676107b639a14 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1429,9 +1429,6 @@ impl<'a> WindowContext<'a> { self.platform.set_cursor_style(cursor_style); } - self.window.drawing = false; - ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear()); - if previous_focus_path != current_focus_path || previous_window_active != current_window_active { @@ -1460,6 +1457,9 @@ impl<'a> WindowContext<'a> { .retain(&(), |listener| listener(&event, self)); } + self.window.drawing = false; + ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear()); + scene } @@ -1904,7 +1904,17 @@ impl<'a> WindowContext<'a> { let mut this = self.to_async(); self.window .platform_window - .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true))) + .on_should_close(Box::new(move || { + this.update(|_, cx| { + // Ensure that the window is removed from the app if it's been closed + // by always pre-empting the system close event. + if f(cx) { + cx.remove_window(); + } + false + }) + .unwrap_or(true) + })) } } diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift index 5f22acf581686d0a3ffc3ae69962c00f271dccb1..db5da8e0e9ec5608e81fe34f3aa901d2e819f21d 100644 --- a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift +++ b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift @@ -12,6 +12,8 @@ class LKRoomDelegate: RoomDelegate { var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void + var onDidPublishOrUnpublishLocalAudioTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void + var onDidPublishOrUnpublishLocalVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void init( data: UnsafeRawPointer, @@ -21,7 +23,10 @@ class LKRoomDelegate: RoomDelegate { onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, - onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) + onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void, + onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void + ) { self.data = data self.onDidDisconnect = onDidDisconnect @@ -31,6 +36,8 @@ class LKRoomDelegate: RoomDelegate { self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack self.onActiveSpeakersChanged = onActiveSpeakersChanged + self.onDidPublishOrUnpublishLocalAudioTrack = onDidPublishOrUnpublishLocalAudioTrack + self.onDidPublishOrUnpublishLocalVideoTrack = onDidPublishOrUnpublishLocalVideoTrack } func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { @@ -65,6 +72,22 @@ class LKRoomDelegate: RoomDelegate { self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString) } } + + func room(_ room: Room, localParticipant: LocalParticipant, didPublish publication: LocalTrackPublication) { + if publication.kind == .video { + self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true) + } else if publication.kind == .audio { + self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true) + } + } + + func room(_ room: Room, localParticipant: LocalParticipant, didUnpublish publication: LocalTrackPublication) { + if publication.kind == .video { + self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false) + } else if publication.kind == .audio { + self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false) + } + } } class LKVideoRenderer: NSObject, VideoRenderer { @@ -109,7 +132,9 @@ public func LKRoomDelegateCreate( onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, - onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void + onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void, + onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void ) -> UnsafeMutableRawPointer { let delegate = LKRoomDelegate( data: data, @@ -119,7 +144,9 @@ public func LKRoomDelegateCreate( onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack, onActiveSpeakersChanged: onActiveSpeakerChanged, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, - onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack + onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack, + onDidPublishOrUnpublishLocalAudioTrack: onDidPublishOrUnpublishLocalAudioTrack, + onDidPublishOrUnpublishLocalVideoTrack: onDidPublishOrUnpublishLocalVideoTrack ) return Unmanaged.passRetained(delegate).toOpaque() } @@ -292,6 +319,14 @@ public func LKLocalTrackPublicationSetMute( } } +@_cdecl("LKLocalTrackPublicationIsMuted") +public func LKLocalTrackPublicationIsMuted( + publication: UnsafeRawPointer +) -> Bool { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + return publication.muted +} + @_cdecl("LKRemoteTrackPublicationSetEnabled") public func LKRemoteTrackPublicationSetEnabled( publication: UnsafeRawPointer, @@ -325,3 +360,12 @@ public func LKRemoteTrackPublicationGetSid( return publication.sid as CFString } + +@_cdecl("LKLocalTrackPublicationGetSid") +public func LKLocalTrackPublicationGetSid( + publication: UnsafeRawPointer +) -> CFString { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + return publication.sid as CFString +} diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index 68a8a84209b1df63f81f972918a7f1dc63e77a74..9fc8aafd30c283df748796790964dab11151d9af 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -2,9 +2,7 @@ use std::{sync::Arc, time::Duration}; use futures::StreamExt; use gpui::{actions, KeyBinding, Menu, MenuItem}; -use live_kit_client::{ - LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, -}; +use live_kit_client::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate}; use live_kit_server::token::{self, VideoGrant}; use log::LevelFilter; use simplelog::SimpleLogger; @@ -60,12 +58,12 @@ fn main() { let room_b = Room::new(); room_b.connect(&live_kit_url, &user2_token).await.unwrap(); - let mut audio_track_updates = room_b.remote_audio_track_updates(); + let mut room_updates = room_b.updates(); let audio_track = LocalAudioTrack::create(); let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap(); - if let RemoteAudioTrackUpdate::Subscribed(track, _) = - audio_track_updates.next().await.unwrap() + if let RoomUpdate::SubscribedToRemoteAudioTrack(track, _) = + room_updates.next().await.unwrap() { let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); assert_eq!(remote_tracks.len(), 1); @@ -78,8 +76,8 @@ fn main() { audio_track_publication.set_mute(true).await.unwrap(); println!("waiting for mute changed!"); - if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = - audio_track_updates.next().await.unwrap() + if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } = + room_updates.next().await.unwrap() { let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); assert_eq!(remote_tracks[0].sid(), track_id); @@ -90,8 +88,8 @@ fn main() { audio_track_publication.set_mute(false).await.unwrap(); - if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = - audio_track_updates.next().await.unwrap() + if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } = + room_updates.next().await.unwrap() { let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); assert_eq!(remote_tracks[0].sid(), track_id); @@ -110,13 +108,13 @@ fn main() { room_a.unpublish_track(audio_track_publication); // Clear out any active speakers changed messages - let mut next = audio_track_updates.next().await.unwrap(); - while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next { + let mut next = room_updates.next().await.unwrap(); + while let RoomUpdate::ActiveSpeakersChanged { speakers } = next { println!("Speakers changed: {:?}", speakers); - next = audio_track_updates.next().await.unwrap(); + next = room_updates.next().await.unwrap(); } - if let RemoteAudioTrackUpdate::Unsubscribed { + if let RoomUpdate::UnsubscribedFromRemoteAudioTrack { publisher_id, track_id, } = next @@ -128,7 +126,6 @@ fn main() { panic!("unexpected message"); } - let mut video_track_updates = room_b.remote_video_track_updates(); let displays = room_a.display_sources().await.unwrap(); let display = displays.into_iter().next().unwrap(); @@ -136,8 +133,8 @@ fn main() { let local_video_track_publication = room_a.publish_video_track(local_video_track).await.unwrap(); - if let RemoteVideoTrackUpdate::Subscribed(track) = - video_track_updates.next().await.unwrap() + if let RoomUpdate::SubscribedToRemoteVideoTrack(track) = + room_updates.next().await.unwrap() { let remote_video_tracks = room_b.remote_video_tracks("test-participant-1"); assert_eq!(remote_video_tracks.len(), 1); @@ -152,10 +149,10 @@ fn main() { .pop() .unwrap(); room_a.unpublish_track(local_video_track_publication); - if let RemoteVideoTrackUpdate::Unsubscribed { + if let RoomUpdate::UnsubscribedFromRemoteVideoTrack { publisher_id, track_id, - } = video_track_updates.next().await.unwrap() + } = room_updates.next().await.unwrap() { assert_eq!(publisher_id, "test-participant-1"); assert_eq!(remote_video_track.sid(), track_id); diff --git a/crates/live_kit_client/src/live_kit_client.rs b/crates/live_kit_client/src/live_kit_client.rs index 47cc3873ff0c1d4f7a3148878be32c12d8f98c5b..abec27462e39639c9f46bd5bedfa7c2433cf10a5 100644 --- a/crates/live_kit_client/src/live_kit_client.rs +++ b/crates/live_kit_client/src/live_kit_client.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + #[cfg(not(any(test, feature = "test-support")))] pub mod prod; @@ -9,3 +11,25 @@ pub mod test; #[cfg(any(test, feature = "test-support"))] pub use test::*; + +pub type Sid = String; + +#[derive(Clone, Eq, PartialEq)] +pub enum ConnectionState { + Disconnected, + Connected { url: String, token: String }, +} + +#[derive(Clone)] +pub enum RoomUpdate { + ActiveSpeakersChanged { speakers: Vec }, + RemoteAudioTrackMuteChanged { track_id: Sid, muted: bool }, + SubscribedToRemoteVideoTrack(Arc), + SubscribedToRemoteAudioTrack(Arc, Arc), + UnsubscribedFromRemoteVideoTrack { publisher_id: Sid, track_id: Sid }, + UnsubscribedFromRemoteAudioTrack { publisher_id: Sid, track_id: Sid }, + LocalAudioTrackPublished { publication: LocalTrackPublication }, + LocalAudioTrackUnpublished { publication: LocalTrackPublication }, + LocalVideoTrackPublished { publication: LocalTrackPublication }, + LocalVideoTrackUnpublished { publication: LocalTrackPublication }, +} diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index 5d8ef9bf134a61111247d3fac5b94fd87a50caf8..0827c0cbb41092f7643891c71c8b8c1f279a0860 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -1,3 +1,4 @@ +use crate::{ConnectionState, RoomUpdate, Sid}; use anyhow::{anyhow, Context, Result}; use core_foundation::{ array::{CFArray, CFArrayRef}, @@ -76,6 +77,16 @@ extern "C" { publisher_id: CFStringRef, track_id: CFStringRef, ), + on_did_publish_or_unpublish_local_audio_track: extern "C" fn( + callback_data: *mut c_void, + publication: swift::LocalTrackPublication, + is_published: bool, + ), + on_did_publish_or_unpublish_local_video_track: extern "C" fn( + callback_data: *mut c_void, + publication: swift::LocalTrackPublication, + is_published: bool, + ), ) -> swift::RoomDelegate; fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room; @@ -151,26 +162,19 @@ extern "C" { callback_data: *mut c_void, ); + fn LKLocalTrackPublicationIsMuted(publication: swift::LocalTrackPublication) -> bool; fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool; + fn LKLocalTrackPublicationGetSid(publication: swift::LocalTrackPublication) -> CFStringRef; fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef; } -pub type Sid = String; - -#[derive(Clone, Eq, PartialEq)] -pub enum ConnectionState { - Disconnected, - Connected { url: String, token: String }, -} - pub struct Room { native_room: swift::Room, connection: Mutex<( watch::Sender, watch::Receiver, )>, - remote_audio_track_subscribers: Mutex>>, - remote_video_track_subscribers: Mutex>>, + update_subscribers: Mutex>>, _delegate: RoomDelegate, } @@ -181,8 +185,7 @@ impl Room { Self { native_room: unsafe { LKRoomCreate(delegate.native_delegate) }, connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)), - remote_audio_track_subscribers: Default::default(), - remote_video_track_subscribers: Default::default(), + update_subscribers: Default::default(), _delegate: delegate, } }) @@ -397,15 +400,9 @@ impl Room { } } - pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver { - let (tx, rx) = mpsc::unbounded(); - self.remote_audio_track_subscribers.lock().push(tx); - rx - } - - pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver { + pub fn updates(&self) -> mpsc::UnboundedReceiver { let (tx, rx) = mpsc::unbounded(); - self.remote_video_track_subscribers.lock().push(tx); + self.update_subscribers.lock().push(tx); rx } @@ -416,8 +413,8 @@ impl Room { ) { let track = Arc::new(track); let publication = Arc::new(publication); - self.remote_audio_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed( + self.update_subscribers.lock().retain(|tx| { + tx.unbounded_send(RoomUpdate::SubscribedToRemoteAudioTrack( track.clone(), publication.clone(), )) @@ -426,8 +423,8 @@ impl Room { } fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) { - self.remote_audio_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed { + self.update_subscribers.lock().retain(|tx| { + tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteAudioTrack { publisher_id: publisher_id.clone(), track_id: track_id.clone(), }) @@ -436,8 +433,8 @@ impl Room { } fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) { - self.remote_audio_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged { + self.update_subscribers.lock().retain(|tx| { + tx.unbounded_send(RoomUpdate::RemoteAudioTrackMuteChanged { track_id: track_id.clone(), muted, }) @@ -445,29 +442,26 @@ impl Room { }); } - // A vec of publisher IDs fn active_speakers_changed(&self, speakers: Vec) { - self.remote_audio_track_subscribers - .lock() - .retain(move |tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged { - speakers: speakers.clone(), - }) - .is_ok() - }); + self.update_subscribers.lock().retain(move |tx| { + tx.unbounded_send(RoomUpdate::ActiveSpeakersChanged { + speakers: speakers.clone(), + }) + .is_ok() + }); } fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) { let track = Arc::new(track); - self.remote_video_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone())) + self.update_subscribers.lock().retain(|tx| { + tx.unbounded_send(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone())) .is_ok() }); } fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) { - self.remote_video_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed { + self.update_subscribers.lock().retain(|tx| { + tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteVideoTrack { publisher_id: publisher_id.clone(), track_id: track_id.clone(), }) @@ -529,6 +523,8 @@ impl RoomDelegate { Self::on_active_speakers_changed, Self::on_did_subscribe_to_remote_video_track, Self::on_did_unsubscribe_from_remote_video_track, + Self::on_did_publish_or_unpublish_local_audio_track, + Self::on_did_publish_or_unpublish_local_video_track, ) }; Self { @@ -642,6 +638,46 @@ impl RoomDelegate { } let _ = Weak::into_raw(room); } + + extern "C" fn on_did_publish_or_unpublish_local_audio_track( + room: *mut c_void, + publication: swift::LocalTrackPublication, + is_published: bool, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + if let Some(room) = room.upgrade() { + let publication = LocalTrackPublication::new(publication); + let update = if is_published { + RoomUpdate::LocalAudioTrackPublished { publication } + } else { + RoomUpdate::LocalAudioTrackUnpublished { publication } + }; + room.update_subscribers + .lock() + .retain(|tx| tx.unbounded_send(update.clone()).is_ok()); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_publish_or_unpublish_local_video_track( + room: *mut c_void, + publication: swift::LocalTrackPublication, + is_published: bool, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + if let Some(room) = room.upgrade() { + let publication = LocalTrackPublication::new(publication); + let update = if is_published { + RoomUpdate::LocalVideoTrackPublished { publication } + } else { + RoomUpdate::LocalVideoTrackUnpublished { publication } + }; + room.update_subscribers + .lock() + .retain(|tx| tx.unbounded_send(update.clone()).is_ok()); + } + let _ = Weak::into_raw(room); + } } impl Drop for RoomDelegate { @@ -691,6 +727,10 @@ impl LocalTrackPublication { Self(native_track_publication) } + pub fn sid(&self) -> String { + unsafe { CFString::wrap_under_get_rule(LKLocalTrackPublicationGetSid(self.0)).to_string() } + } + pub fn set_mute(&self, muted: bool) -> impl Future> { let (tx, rx) = futures::channel::oneshot::channel(); @@ -715,6 +755,19 @@ impl LocalTrackPublication { async move { rx.await.unwrap() } } + + pub fn is_muted(&self) -> bool { + unsafe { LKLocalTrackPublicationIsMuted(self.0) } + } +} + +impl Clone for LocalTrackPublication { + fn clone(&self) -> Self { + unsafe { + CFRetain(self.0 .0); + } + Self(self.0) + } } impl Drop for LocalTrackPublication { @@ -889,18 +942,6 @@ impl Drop for RemoteVideoTrack { } } -pub enum RemoteVideoTrackUpdate { - Subscribed(Arc), - Unsubscribed { publisher_id: Sid, track_id: Sid }, -} - -pub enum RemoteAudioTrackUpdate { - ActiveSpeakersChanged { speakers: Vec }, - MuteChanged { track_id: Sid, muted: bool }, - Subscribed(Arc, Arc), - Unsubscribed { publisher_id: Sid, track_id: Sid }, -} - pub struct MacOSDisplay(swift::MacOSDisplay); impl MacOSDisplay { diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 4575fdd2c1845c53b29876d83f1fb6a8498717dc..0716042ff196e1e0ea8f3543f03b645648ed473c 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -1,3 +1,4 @@ +use crate::{ConnectionState, RoomUpdate, Sid}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{BTreeMap, HashMap}; @@ -7,7 +8,14 @@ use live_kit_server::{proto, token}; use media::core_video::CVImageBuffer; use parking_lot::Mutex; use postage::watch; -use std::{future::Future, mem, sync::Arc}; +use std::{ + future::Future, + mem, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; static SERVERS: Mutex>> = Mutex::new(BTreeMap::new()); @@ -104,9 +112,8 @@ impl TestServer { client_room .0 .lock() - .video_track_updates - .0 - .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone())) + .updates_tx + .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone())) .unwrap(); } room.client_rooms.insert(identity, client_room); @@ -176,7 +183,11 @@ impl TestServer { } } - async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> { + async fn publish_video_track( + &self, + token: String, + local_track: LocalVideoTrack, + ) -> Result { self.executor.simulate_random_delay().await; let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); @@ -198,8 +209,9 @@ impl TestServer { return Err(anyhow!("user is not allowed to publish")); } + let sid = nanoid::nanoid!(17); let track = Arc::new(RemoteVideoTrack { - sid: nanoid::nanoid!(17), + sid: sid.clone(), publisher_id: identity.clone(), frames_rx: local_track.frames_rx.clone(), }); @@ -211,21 +223,20 @@ impl TestServer { let _ = client_room .0 .lock() - .video_track_updates - .0 - .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone())) + .updates_tx + .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone())) .unwrap(); } } - Ok(()) + Ok(sid) } async fn publish_audio_track( &self, token: String, _local_track: &LocalAudioTrack, - ) -> Result<()> { + ) -> Result { self.executor.simulate_random_delay().await; let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); @@ -247,8 +258,9 @@ impl TestServer { return Err(anyhow!("user is not allowed to publish")); } + let sid = nanoid::nanoid!(17); let track = Arc::new(RemoteAudioTrack { - sid: nanoid::nanoid!(17), + sid: sid.clone(), publisher_id: identity.clone(), }); @@ -261,9 +273,8 @@ impl TestServer { let _ = client_room .0 .lock() - .audio_track_updates - .0 - .try_broadcast(RemoteAudioTrackUpdate::Subscribed( + .updates_tx + .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack( track.clone(), publication.clone(), )) @@ -271,7 +282,7 @@ impl TestServer { } } - Ok(()) + Ok(sid) } fn video_tracks(&self, token: String) -> Result>> { @@ -369,39 +380,26 @@ impl live_kit_server::api::Client for TestApiClient { } } -pub type Sid = String; - struct RoomState { connection: ( watch::Sender, watch::Receiver, ), display_sources: Vec, - audio_track_updates: ( - async_broadcast::Sender, - async_broadcast::Receiver, - ), - video_track_updates: ( - async_broadcast::Sender, - async_broadcast::Receiver, - ), -} - -#[derive(Clone, Eq, PartialEq)] -pub enum ConnectionState { - Disconnected, - Connected { url: String, token: String }, + updates_tx: async_broadcast::Sender, + updates_rx: async_broadcast::Receiver, } pub struct Room(Mutex); impl Room { pub fn new() -> Arc { + let (updates_tx, updates_rx) = async_broadcast::broadcast(128); Arc::new(Self(Mutex::new(RoomState { connection: watch::channel_with(ConnectionState::Disconnected), display_sources: Default::default(), - video_track_updates: async_broadcast::broadcast(128), - audio_track_updates: async_broadcast::broadcast(128), + updates_tx, + updates_rx, }))) } @@ -440,10 +438,14 @@ impl Room { let this = self.clone(); let track = track.clone(); async move { - this.test_server() + let sid = this + .test_server() .publish_video_track(this.token(), track) .await?; - Ok(LocalTrackPublication) + Ok(LocalTrackPublication { + muted: Default::default(), + sid, + }) } } pub fn publish_audio_track( @@ -453,10 +455,14 @@ impl Room { let this = self.clone(); let track = track.clone(); async move { - this.test_server() + let sid = this + .test_server() .publish_audio_track(this.token(), &track) .await?; - Ok(LocalTrackPublication) + Ok(LocalTrackPublication { + muted: Default::default(), + sid, + }) } } @@ -505,12 +511,8 @@ impl Room { .collect() } - pub fn remote_audio_track_updates(&self) -> impl Stream { - self.0.lock().audio_track_updates.1.clone() - } - - pub fn remote_video_track_updates(&self) -> impl Stream { - self.0.lock().video_track_updates.1.clone() + pub fn updates(&self) -> impl Stream { + self.0.lock().updates_rx.clone() } pub fn set_display_sources(&self, sources: Vec) { @@ -555,11 +557,27 @@ impl Drop for Room { } } -pub struct LocalTrackPublication; +#[derive(Clone)] +pub struct LocalTrackPublication { + sid: String, + muted: Arc, +} impl LocalTrackPublication { - pub fn set_mute(&self, _mute: bool) -> impl Future> { - async { Ok(()) } + pub fn set_mute(&self, mute: bool) -> impl Future> { + let muted = self.muted.clone(); + async move { + muted.store(mute, SeqCst); + Ok(()) + } + } + + pub fn is_muted(&self) -> bool { + self.muted.load(SeqCst) + } + + pub fn sid(&self) -> String { + self.sid.clone() } } @@ -646,20 +664,6 @@ impl RemoteAudioTrack { } } -#[derive(Clone)] -pub enum RemoteVideoTrackUpdate { - Subscribed(Arc), - Unsubscribed { publisher_id: Sid, track_id: Sid }, -} - -#[derive(Clone)] -pub enum RemoteAudioTrackUpdate { - ActiveSpeakersChanged { speakers: Vec }, - MuteChanged { track_id: Sid, muted: bool }, - Subscribed(Arc, Arc), - Unsubscribed { publisher_id: Sid, track_id: Sid }, -} - #[derive(Clone)] pub struct MacOSDisplay { frames: ( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c412fad0e10ba07ee4849273577d6b2425089820..06b6da75b3ed382d03becbb5a418ddf28cbc05c0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -130,7 +130,7 @@ pub struct Project { next_diagnostic_group_id: usize, user_store: Model, fs: Arc, - client_state: Option, + client_state: ProjectClientState, collaborators: HashMap, client_subscriptions: Vec, _subscriptions: Vec, @@ -254,8 +254,10 @@ enum WorktreeHandle { Weak(WeakModel), } +#[derive(Debug)] enum ProjectClientState { - Local { + Local, + Shared { remote_id: u64, updates_tx: mpsc::UnboundedSender, _send_updates: Task>, @@ -657,7 +659,7 @@ impl Project { local_buffer_ids_by_entry_id: Default::default(), buffer_snapshots: Default::default(), join_project_response_message_id: 0, - client_state: None, + client_state: ProjectClientState::Local, opened_buffer: watch::channel(), client_subscriptions: Vec::new(), _subscriptions: vec![ @@ -756,12 +758,12 @@ impl Project { cx.on_app_quit(Self::shutdown_language_servers), ], client: client.clone(), - client_state: Some(ProjectClientState::Remote { + client_state: ProjectClientState::Remote { sharing_has_stopped: false, capability: Capability::ReadWrite, remote_id, replica_id, - }), + }, supplementary_language_servers: HashMap::default(), language_servers: Default::default(), language_server_ids: Default::default(), @@ -828,16 +830,16 @@ impl Project { fn release(&mut self, cx: &mut AppContext) { match &self.client_state { - Some(ProjectClientState::Local { .. }) => { + ProjectClientState::Local => {} + ProjectClientState::Shared { .. } => { let _ = self.unshare_internal(cx); } - Some(ProjectClientState::Remote { remote_id, .. }) => { + ProjectClientState::Remote { remote_id, .. } => { let _ = self.client.send(proto::LeaveProject { project_id: *remote_id, }); self.disconnected_from_host_internal(cx); } - _ => {} } } @@ -1058,21 +1060,22 @@ impl Project { } pub fn remote_id(&self) -> Option { - match self.client_state.as_ref()? { - ProjectClientState::Local { remote_id, .. } - | ProjectClientState::Remote { remote_id, .. } => Some(*remote_id), + match self.client_state { + ProjectClientState::Local => None, + ProjectClientState::Shared { remote_id, .. } + | ProjectClientState::Remote { remote_id, .. } => Some(remote_id), } } pub fn replica_id(&self) -> ReplicaId { - match &self.client_state { - Some(ProjectClientState::Remote { replica_id, .. }) => *replica_id, + match self.client_state { + ProjectClientState::Remote { replica_id, .. } => replica_id, _ => 0, } } fn metadata_changed(&mut self, cx: &mut ModelContext) { - if let Some(ProjectClientState::Local { updates_tx, .. }) = &mut self.client_state { + if let ProjectClientState::Shared { updates_tx, .. } = &mut self.client_state { updates_tx .unbounded_send(LocalProjectUpdate::WorktreesChanged) .ok(); @@ -1362,7 +1365,7 @@ impl Project { } pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext) -> Result<()> { - if self.client_state.is_some() { + if !matches!(self.client_state, ProjectClientState::Local) { return Err(anyhow!("project was already shared")); } self.client_subscriptions.push( @@ -1423,7 +1426,7 @@ impl Project { let (updates_tx, mut updates_rx) = mpsc::unbounded(); let client = self.client.clone(); - self.client_state = Some(ProjectClientState::Local { + self.client_state = ProjectClientState::Shared { remote_id: project_id, updates_tx, _send_updates: cx.spawn(move |this, mut cx| async move { @@ -1508,7 +1511,7 @@ impl Project { } Ok(()) }), - }); + }; self.metadata_changed(cx); cx.emit(Event::RemoteIdChanged(Some(project_id))); @@ -1578,7 +1581,8 @@ impl Project { return Err(anyhow!("attempted to unshare a remote project")); } - if let Some(ProjectClientState::Local { remote_id, .. }) = self.client_state.take() { + if let ProjectClientState::Shared { remote_id, .. } = self.client_state { + self.client_state = ProjectClientState::Local; self.collaborators.clear(); self.shared_buffers.clear(); self.client_subscriptions.clear(); @@ -1629,23 +1633,23 @@ impl Project { } else { Capability::ReadOnly }; - if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { + if let ProjectClientState::Remote { capability, .. } = &mut self.client_state { if *capability == new_capability { return; } *capability = new_capability; - } - for buffer in self.opened_buffers() { - buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx)); + for buffer in self.opened_buffers() { + buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx)); + } } } fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) { - if let Some(ProjectClientState::Remote { + if let ProjectClientState::Remote { sharing_has_stopped, .. - }) = &mut self.client_state + } = &mut self.client_state { *sharing_has_stopped = true; @@ -1684,18 +1688,18 @@ impl Project { pub fn is_disconnected(&self) -> bool { match &self.client_state { - Some(ProjectClientState::Remote { + ProjectClientState::Remote { sharing_has_stopped, .. - }) => *sharing_has_stopped, + } => *sharing_has_stopped, _ => false, } } pub fn capability(&self) -> Capability { match &self.client_state { - Some(ProjectClientState::Remote { capability, .. }) => *capability, - Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite, + ProjectClientState::Remote { capability, .. } => *capability, + ProjectClientState::Shared { .. } | ProjectClientState::Local => Capability::ReadWrite, } } @@ -1705,8 +1709,8 @@ impl Project { pub fn is_local(&self) -> bool { match &self.client_state { - Some(ProjectClientState::Remote { .. }) => false, - _ => true, + ProjectClientState::Local | ProjectClientState::Shared { .. } => true, + ProjectClientState::Remote { .. } => false, } } @@ -6165,8 +6169,8 @@ impl Project { pub fn is_shared(&self) -> bool { match &self.client_state { - Some(ProjectClientState::Local { .. }) => true, - _ => false, + ProjectClientState::Shared { .. } => true, + ProjectClientState::Local | ProjectClientState::Remote { .. } => false, } } @@ -7954,7 +7958,7 @@ impl Project { cx: &mut AppContext, ) -> u64 { let buffer_id = buffer.read(cx).remote_id(); - if let Some(ProjectClientState::Local { updates_tx, .. }) = &self.client_state { + if let ProjectClientState::Shared { updates_tx, .. } = &self.client_state { updates_tx .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id }) .ok(); @@ -8003,21 +8007,21 @@ impl Project { } fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext) -> Task> { - let project_id = match self.client_state.as_ref() { - Some(ProjectClientState::Remote { + let project_id = match self.client_state { + ProjectClientState::Remote { sharing_has_stopped, remote_id, .. - }) => { - if *sharing_has_stopped { + } => { + if sharing_has_stopped { return Task::ready(Err(anyhow!( "can't synchronize remote buffers on a readonly project" ))); } else { - *remote_id + remote_id } } - Some(ProjectClientState::Local { .. }) | None => { + ProjectClientState::Shared { .. } | ProjectClientState::Local => { return Task::ready(Err(anyhow!( "can't synchronize remote buffers on a local project" ))) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7ef21c42ed3886a81ad6b9b08a17f626d99acb1f..f7e36fe696258fa1a361bc6bd212f1d888efc2f8 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1130,7 +1130,6 @@ mod tests { #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); - // todo! osiewicz: these tests asserted on background color as well, that should be brought back. let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { background_highlights .into_iter() @@ -1395,7 +1394,6 @@ mod tests { }) .await .unwrap(); - // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back. let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { background_highlights .into_iter() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6fd66b5bad2c1c9ab1036a445f1e2061b791f206..8897ae4bcfcd2ec2f14266dac81d97e8cf161985 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1015,9 +1015,6 @@ impl ProjectSearchView { workspace.add_item(Box::new(view.clone()), cx); view }; - - workspace.add_item(Box::new(search.clone()), cx); - search.update(cx, |search, cx| { if let Some(query) = query { search.set_query(&query, cx); @@ -1984,6 +1981,7 @@ pub mod tests { use semantic_index::semantic_index_settings::SemanticIndexSettings; use serde_json::json; use settings::{Settings, SettingsStore}; + use workspace::DeploySearch; #[gpui::test] async fn test_project_search(cx: &mut TestAppContext) { @@ -3111,6 +3109,124 @@ pub mod tests { .unwrap(); } + #[gpui::test] + async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let worktree_id = project.update(cx, |this, cx| { + this.worktrees().next().unwrap().read(cx).id() + }); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let panes: Vec<_> = window + .update(cx, |this, _| this.panes().to_owned()) + .unwrap(); + assert_eq!(panes.len(), 1); + let first_pane = panes.get(0).cloned().unwrap(); + assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); + window + .update(cx, |workspace, cx| { + workspace.open_path( + (worktree_id, "one.rs"), + Some(first_pane.downgrade()), + true, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); + let second_pane = window + .update(cx, |workspace, cx| { + workspace.split_and_clone(first_pane.clone(), workspace::SplitDirection::Right, cx) + }) + .unwrap() + .unwrap(); + assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); + assert!(window + .update(cx, |_, cx| second_pane + .focus_handle(cx) + .contains_focused(cx)) + .unwrap()); + let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); + window + .update(cx, { + let search_bar = search_bar.clone(); + let pane = first_pane.clone(); + move |workspace, cx| { + assert_eq!(workspace.panes().len(), 2); + pane.update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + } + }) + .unwrap(); + window + .update(cx, { + let search_bar = search_bar.clone(); + let pane = second_pane.clone(); + move |workspace, cx| { + assert_eq!(workspace.panes().len(), 2); + pane.update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + + ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx) + } + }) + .unwrap(); + + cx.run_until_parked(); + assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); + assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); + window + .update(cx, |workspace, cx| { + assert_eq!(workspace.active_pane(), &second_pane); + second_pane.update(cx, |this, cx| { + assert_eq!(this.active_item_index(), 1); + this.activate_prev_item(false, cx); + assert_eq!(this.active_item_index(), 0); + }); + workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx); + }) + .unwrap(); + window + .update(cx, |workspace, cx| { + assert_eq!(workspace.active_pane(), &first_pane); + assert_eq!(first_pane.read(cx).items_len(), 1); + assert_eq!(second_pane.read(cx).items_len(), 2); + }) + .unwrap(); + cx.dispatch_action(window.into(), DeploySearch); + + // We should have same # of items in workspace, the only difference being that + // the search we've deployed previously should now be focused. + window + .update(cx, |workspace, cx| { + assert_eq!(workspace.active_pane(), &second_pane); + second_pane.update(cx, |this, _| { + assert_eq!(this.active_item_index(), 1); + assert_eq!(this.items_len(), 2); + }); + first_pane.update(cx, |this, cx| { + assert!(!cx.focus_handle().contains_focused(cx)); + assert_eq!(this.items_len(), 1); + }); + }) + .unwrap(); + } + pub fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c52dbcb3d8e453729bd0d1ab57dc8da94e131f58..3e72acc51bd4b442514e61c18d4755fa868f8fe6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -6,7 +6,7 @@ use gpui::{ InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, - WhiteSpace, WindowContext, + WeakView, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -24,6 +24,7 @@ use terminal::{ }; use theme::{ActiveTheme, Theme, ThemeSettings}; use ui::Tooltip; +use workspace::Workspace; use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; @@ -142,6 +143,7 @@ impl LayoutRect { ///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? pub struct TerminalElement { terminal: Model, + workspace: WeakView, focus: FocusHandle, focused: bool, cursor_visible: bool, @@ -160,6 +162,7 @@ impl StatefulInteractiveElement for TerminalElement {} impl TerminalElement { pub fn new( terminal: Model, + workspace: WeakView, focus: FocusHandle, focused: bool, cursor_visible: bool, @@ -167,6 +170,7 @@ impl TerminalElement { ) -> TerminalElement { TerminalElement { terminal, + workspace, focused, focus: focus.clone(), cursor_visible, @@ -762,6 +766,7 @@ impl Element for TerminalElement { .cursor .as_ref() .map(|cursor| cursor.bounding_rect(origin)), + workspace: self.workspace.clone(), }; self.register_mouse_listeners(origin, layout.mode, bounds, cx); @@ -831,6 +836,7 @@ impl IntoElement for TerminalElement { struct TerminalInputHandler { cx: AsyncWindowContext, terminal: Model, + workspace: WeakView, cursor_bounds: Option>, } @@ -871,7 +877,14 @@ impl PlatformInputHandler for TerminalInputHandler { .update(|_, cx| { self.terminal.update(cx, |terminal, _| { terminal.input(text.into()); - }) + }); + + self.workspace + .update(cx, |this, cx| { + let telemetry = this.project().read(cx).client().telemetry().clone(); + telemetry.log_edit_event("terminal"); + }) + .ok(); }) .ok(); } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index b4a273dd0bc9085c4a6b3b069589ef1d9d640c5d..ced122402f138e5f5b964792d7a1561260b063b6 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -73,6 +73,7 @@ pub fn init(cx: &mut AppContext) { ///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct TerminalView { terminal: Model, + workspace: WeakView, focus_handle: FocusHandle, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received @@ -135,6 +136,7 @@ impl TerminalView { workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { + let workspace_handle = workspace.clone(); cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); cx.subscribe(&terminal, move |this, _, event, cx| match event { Event::Wakeup => { @@ -279,6 +281,7 @@ impl TerminalView { Self { terminal, + workspace: workspace_handle, has_new_content: true, has_bell: false, focus_handle: cx.focus_handle(), @@ -661,6 +664,7 @@ impl Render for TerminalView { // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu div().size_full().child(TerminalElement::new( terminal_handle, + self.workspace.clone(), self.focus_handle.clone(), focused, self.should_show_cursor(focused, cx), diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index b2b797db08b270513b77809437184f9b782af77d..508b091b8b69c5e3ffb995e87862bffe0cbf5ae5 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -22,7 +22,7 @@ pub struct PlayerColors(pub Vec); impl Default for PlayerColors { /// Don't use this! /// We have to have a default to be `[refineable::Refinable]`. - /// todo!("Find a way to not need this for Refinable") + /// TODO "Find a way to not need this for Refinable" fn default() -> Self { Self::dark() } diff --git a/crates/theme/src/themes/andromeda.rs b/crates/theme/src/themes/andromeda.rs index effdfb85f97a0c9baa1d4b9881b6c89de9ec1245..45dc66094518c5ab25fe2ad12e616213e4520994 100644 --- a/crates/theme/src/themes/andromeda.rs +++ b/crates/theme/src/themes/andromeda.rs @@ -69,7 +69,7 @@ pub fn andromeda() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x21242bff).into()), editor_line_number: Some(rgba(0xf7f7f859).into()), editor_active_line_number: Some(rgba(0xf7f7f8ff).into()), - editor_invisible: Some(rgba(0xaca8aeff).into()), + editor_invisible: Some(rgba(0x64646dff).into()), editor_wrap_guide: Some(rgba(0xf7f7f80d).into()), editor_active_wrap_guide: Some(rgba(0xf7f7f81a).into()), editor_document_highlight_read_background: Some(rgba(0x11a7931a).into()), diff --git a/crates/theme/src/themes/atelier.rs b/crates/theme/src/themes/atelier.rs index 6848676e00734821a4bf44f5b6c6a67cd5bf54ac..e0682b217e0442a53d7c25e6ac8e1339fc55524b 100644 --- a/crates/theme/src/themes/atelier.rs +++ b/crates/theme/src/themes/atelier.rs @@ -70,7 +70,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x221f26ff).into()), editor_line_number: Some(rgba(0xefecf459).into()), editor_active_line_number: Some(rgba(0xefecf4ff).into()), - editor_invisible: Some(rgba(0x898591ff).into()), + editor_invisible: Some(rgba(0x726c7aff).into()), editor_wrap_guide: Some(rgba(0xefecf40d).into()), editor_active_wrap_guide: Some(rgba(0xefecf41a).into()), editor_document_highlight_read_background: Some(rgba(0x576dda1a).into()), @@ -535,7 +535,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xe6e3ebff).into()), editor_line_number: Some(rgba(0x19171c59).into()), editor_active_line_number: Some(rgba(0x19171cff).into()), - editor_invisible: Some(rgba(0x5a5462ff).into()), + editor_invisible: Some(rgba(0x726c7aff).into()), editor_wrap_guide: Some(rgba(0x19171c0d).into()), editor_active_wrap_guide: Some(rgba(0x19171c1a).into()), editor_document_highlight_read_background: Some(rgba(0x586dda1a).into()), @@ -1000,7 +1000,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x262622ff).into()), editor_line_number: Some(rgba(0xfefbec59).into()), editor_active_line_number: Some(rgba(0xfefbecff).into()), - editor_invisible: Some(rgba(0xa4a08bff).into()), + editor_invisible: Some(rgba(0x8b8874ff).into()), editor_wrap_guide: Some(rgba(0xfefbec0d).into()), editor_active_wrap_guide: Some(rgba(0xfefbec1a).into()), editor_document_highlight_read_background: Some(rgba(0x6684e01a).into()), @@ -1465,7 +1465,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xeeebd7ff).into()), editor_line_number: Some(rgba(0x20201d59).into()), editor_active_line_number: Some(rgba(0x20201dff).into()), - editor_invisible: Some(rgba(0x706d5fff).into()), + editor_invisible: Some(rgba(0x8b8874ff).into()), editor_wrap_guide: Some(rgba(0x20201d0d).into()), editor_active_wrap_guide: Some(rgba(0x20201d1a).into()), editor_document_highlight_read_background: Some(rgba(0x6784e01a).into()), @@ -1930,7 +1930,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x2c2b23ff).into()), editor_line_number: Some(rgba(0xf4f3ec59).into()), editor_active_line_number: Some(rgba(0xf4f3ecff).into()), - editor_invisible: Some(rgba(0x91907fff).into()), + editor_invisible: Some(rgba(0x7a7867ff).into()), editor_wrap_guide: Some(rgba(0xf4f3ec0d).into()), editor_active_wrap_guide: Some(rgba(0xf4f3ec1a).into()), editor_document_highlight_read_background: Some(rgba(0x37a1661a).into()), @@ -2395,7 +2395,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xebeae3ff).into()), editor_line_number: Some(rgba(0x22221b59).into()), editor_active_line_number: Some(rgba(0x22221bff).into()), - editor_invisible: Some(rgba(0x61604fff).into()), + editor_invisible: Some(rgba(0x7a7867ff).into()), editor_wrap_guide: Some(rgba(0x22221b0d).into()), editor_active_wrap_guide: Some(rgba(0x22221b1a).into()), editor_document_highlight_read_background: Some(rgba(0x38a1661a).into()), @@ -2860,7 +2860,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x27211eff).into()), editor_line_number: Some(rgba(0xf1efee59).into()), editor_active_line_number: Some(rgba(0xf1efeeff).into()), - editor_invisible: Some(rgba(0xa79f9dff).into()), + editor_invisible: Some(rgba(0x89817eff).into()), editor_wrap_guide: Some(rgba(0xf1efee0d).into()), editor_active_wrap_guide: Some(rgba(0xf1efee1a).into()), editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()), @@ -3325,7 +3325,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xe9e6e4ff).into()), editor_line_number: Some(rgba(0x1b191859).into()), editor_active_line_number: Some(rgba(0x1b1918ff).into()), - editor_invisible: Some(rgba(0x6a6360ff).into()), + editor_invisible: Some(rgba(0x89817eff).into()), editor_wrap_guide: Some(rgba(0x1b19180d).into()), editor_active_wrap_guide: Some(rgba(0x1b19181a).into()), editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()), @@ -3790,7 +3790,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x252025ff).into()), editor_line_number: Some(rgba(0xf7f3f759).into()), editor_active_line_number: Some(rgba(0xf7f3f7ff).into()), - editor_invisible: Some(rgba(0xa99aa9ff).into()), + editor_invisible: Some(rgba(0x8b7c8bff).into()), editor_wrap_guide: Some(rgba(0xf7f3f70d).into()), editor_active_wrap_guide: Some(rgba(0xf7f3f71a).into()), editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()), @@ -4255,7 +4255,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xe1d6e1ff).into()), editor_line_number: Some(rgba(0x1b181b59).into()), editor_active_line_number: Some(rgba(0x1b181bff).into()), - editor_invisible: Some(rgba(0x6b5e6bff).into()), + editor_invisible: Some(rgba(0x8b7c8bff).into()), editor_wrap_guide: Some(rgba(0x1b181b0d).into()), editor_active_wrap_guide: Some(rgba(0x1b181b1a).into()), editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()), @@ -4720,7 +4720,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x1c2529ff).into()), editor_line_number: Some(rgba(0xebf8ff59).into()), editor_active_line_number: Some(rgba(0xebf8ffff).into()), - editor_invisible: Some(rgba(0x7ca0b3ff).into()), + editor_invisible: Some(rgba(0x66889aff).into()), editor_wrap_guide: Some(rgba(0xebf8ff0d).into()), editor_active_wrap_guide: Some(rgba(0xebf8ff1a).into()), editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()), @@ -5185,7 +5185,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xcdeaf9ff).into()), editor_line_number: Some(rgba(0x161b1d59).into()), editor_active_line_number: Some(rgba(0x161b1dff).into()), - editor_invisible: Some(rgba(0x526f7dff).into()), + editor_invisible: Some(rgba(0x66889aff).into()), editor_wrap_guide: Some(rgba(0x161b1d0d).into()), editor_active_wrap_guide: Some(rgba(0x161b1d1a).into()), editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()), @@ -5650,7 +5650,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x252020ff).into()), editor_line_number: Some(rgba(0xf4ecec59).into()), editor_active_line_number: Some(rgba(0xf4ececff).into()), - editor_invisible: Some(rgba(0x898383ff).into()), + editor_invisible: Some(rgba(0x726a6aff).into()), editor_wrap_guide: Some(rgba(0xf4ecec0d).into()), editor_active_wrap_guide: Some(rgba(0xf4ecec1a).into()), editor_document_highlight_read_background: Some(rgba(0x7272ca1a).into()), @@ -6115,7 +6115,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xebe3e3ff).into()), editor_line_number: Some(rgba(0x1b181859).into()), editor_active_line_number: Some(rgba(0x1b1818ff).into()), - editor_invisible: Some(rgba(0x5a5252ff).into()), + editor_invisible: Some(rgba(0x726a6aff).into()), editor_wrap_guide: Some(rgba(0x1b18180d).into()), editor_active_wrap_guide: Some(rgba(0x1b18181a).into()), editor_document_highlight_read_background: Some(rgba(0x7372ca1a).into()), @@ -6580,7 +6580,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x1f2621ff).into()), editor_line_number: Some(rgba(0xecf4ee59).into()), editor_active_line_number: Some(rgba(0xecf4eeff).into()), - editor_invisible: Some(rgba(0x859188ff).into()), + editor_invisible: Some(rgba(0x6c7a71ff).into()), editor_wrap_guide: Some(rgba(0xecf4ee0d).into()), editor_active_wrap_guide: Some(rgba(0xecf4ee1a).into()), editor_document_highlight_read_background: Some(rgba(0x478c901a).into()), @@ -7045,7 +7045,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xe3ebe6ff).into()), editor_line_number: Some(rgba(0x171c1959).into()), editor_active_line_number: Some(rgba(0x171c19ff).into()), - editor_invisible: Some(rgba(0x546259ff).into()), + editor_invisible: Some(rgba(0x6c7a71ff).into()), editor_wrap_guide: Some(rgba(0x171c190d).into()), editor_active_wrap_guide: Some(rgba(0x171c191a).into()), editor_document_highlight_read_background: Some(rgba(0x488c901a).into()), @@ -7510,7 +7510,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x1f231fff).into()), editor_line_number: Some(rgba(0xf4fbf459).into()), editor_active_line_number: Some(rgba(0xf4fbf4ff).into()), - editor_invisible: Some(rgba(0x8ba48bff).into()), + editor_invisible: Some(rgba(0x748b74ff).into()), editor_wrap_guide: Some(rgba(0xf4fbf40d).into()), editor_active_wrap_guide: Some(rgba(0xf4fbf41a).into()), editor_document_highlight_read_background: Some(rgba(0x3e62f41a).into()), @@ -7975,7 +7975,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xdaeedaff).into()), editor_line_number: Some(rgba(0x13151359).into()), editor_active_line_number: Some(rgba(0x131513ff).into()), - editor_invisible: Some(rgba(0x5f705fff).into()), + editor_invisible: Some(rgba(0x748b74ff).into()), editor_wrap_guide: Some(rgba(0x1315130d).into()), editor_active_wrap_guide: Some(rgba(0x1315131a).into()), editor_document_highlight_read_background: Some(rgba(0x3f62f41a).into()), @@ -8440,7 +8440,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x262f51ff).into()), editor_line_number: Some(rgba(0xf5f7ff59).into()), editor_active_line_number: Some(rgba(0xf5f7ffff).into()), - editor_invisible: Some(rgba(0x959bb2ff).into()), + editor_invisible: Some(rgba(0x7a819cff).into()), editor_wrap_guide: Some(rgba(0xf5f7ff0d).into()), editor_active_wrap_guide: Some(rgba(0xf5f7ff1a).into()), editor_document_highlight_read_background: Some(rgba(0x3e8fd01a).into()), @@ -8905,7 +8905,7 @@ pub fn atelier() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xe5e8f5ff).into()), editor_line_number: Some(rgba(0x20274659).into()), editor_active_line_number: Some(rgba(0x202746ff).into()), - editor_invisible: Some(rgba(0x606889ff).into()), + editor_invisible: Some(rgba(0x7a819cff).into()), editor_wrap_guide: Some(rgba(0x2027460d).into()), editor_active_wrap_guide: Some(rgba(0x2027461a).into()), editor_document_highlight_read_background: Some(rgba(0x3f8fd01a).into()), diff --git a/crates/theme/src/themes/ayu.rs b/crates/theme/src/themes/ayu.rs index a0402825c1a1eaf5e6f91986c47505c68772c743..9c9030b2f27928c5e9fb16a6e536de84036c4f8e 100644 --- a/crates/theme/src/themes/ayu.rs +++ b/crates/theme/src/themes/ayu.rs @@ -70,7 +70,7 @@ pub fn ayu() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x1f2127ff).into()), editor_line_number: Some(rgba(0xbfbdb659).into()), editor_active_line_number: Some(rgba(0xbfbdb6ff).into()), - editor_invisible: Some(rgba(0x8a8986ff).into()), + editor_invisible: Some(rgba(0x666767ff).into()), editor_wrap_guide: Some(rgba(0xbfbdb60d).into()), editor_active_wrap_guide: Some(rgba(0xbfbdb61a).into()), editor_document_highlight_read_background: Some(rgba(0x5ac2fe1a).into()), @@ -514,7 +514,7 @@ pub fn ayu() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xececedff).into()), editor_line_number: Some(rgba(0x5c616659).into()), editor_active_line_number: Some(rgba(0x5c6166ff).into()), - editor_invisible: Some(rgba(0x8c8f93ff).into()), + editor_invisible: Some(rgba(0xacafb1ff).into()), editor_wrap_guide: Some(rgba(0x5c61660d).into()), editor_active_wrap_guide: Some(rgba(0x5c61661a).into()), editor_document_highlight_read_background: Some(rgba(0x3b9ee51a).into()), @@ -958,7 +958,7 @@ pub fn ayu() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x353944ff).into()), editor_line_number: Some(rgba(0xcccac259).into()), editor_active_line_number: Some(rgba(0xcccac2ff).into()), - editor_invisible: Some(rgba(0x9a9a98ff).into()), + editor_invisible: Some(rgba(0x787a7cff).into()), editor_wrap_guide: Some(rgba(0xcccac20d).into()), editor_active_wrap_guide: Some(rgba(0xcccac21a).into()), editor_document_highlight_read_background: Some(rgba(0x73cffe1a).into()), diff --git a/crates/theme/src/themes/gruvbox.rs b/crates/theme/src/themes/gruvbox.rs index 5a319aa37a69b07998fd06e082cf0f370376abb0..34ccefee1172cb9aee9d85496f2fdce40ab4abd2 100644 --- a/crates/theme/src/themes/gruvbox.rs +++ b/crates/theme/src/themes/gruvbox.rs @@ -70,7 +70,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x3a3735ff).into()), editor_line_number: Some(rgba(0xfbf1c759).into()), editor_active_line_number: Some(rgba(0xfbf1c7ff).into()), - editor_invisible: Some(rgba(0xc5b597ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0xfbf1c70d).into()), editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()), editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), @@ -521,7 +521,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x393634ff).into()), editor_line_number: Some(rgba(0xfbf1c759).into()), editor_active_line_number: Some(rgba(0xfbf1c7ff).into()), - editor_invisible: Some(rgba(0xc5b597ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0xfbf1c70d).into()), editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()), editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), @@ -972,7 +972,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x3b3735ff).into()), editor_line_number: Some(rgba(0xfbf1c759).into()), editor_active_line_number: Some(rgba(0xfbf1c7ff).into()), - editor_invisible: Some(rgba(0xc5b597ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0xfbf1c70d).into()), editor_active_wrap_guide: Some(rgba(0xfbf1c71a).into()), editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), @@ -1423,7 +1423,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xecddb4ff).into()), editor_line_number: Some(rgba(0x28282859).into()), editor_active_line_number: Some(rgba(0x282828ff).into()), - editor_invisible: Some(rgba(0x5f5650ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0x2828280d).into()), editor_active_wrap_guide: Some(rgba(0x2828281a).into()), editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), @@ -1874,7 +1874,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xecddb5ff).into()), editor_line_number: Some(rgba(0x28282859).into()), editor_active_line_number: Some(rgba(0x282828ff).into()), - editor_invisible: Some(rgba(0x5f5650ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0x2828280d).into()), editor_active_wrap_guide: Some(rgba(0x2828281a).into()), editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), @@ -2325,7 +2325,7 @@ pub fn gruvbox() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xecdcb3ff).into()), editor_line_number: Some(rgba(0x28282859).into()), editor_active_line_number: Some(rgba(0x282828ff).into()), - editor_invisible: Some(rgba(0x5f5650ff).into()), + editor_invisible: Some(rgba(0x928474ff).into()), editor_wrap_guide: Some(rgba(0x2828280d).into()), editor_active_wrap_guide: Some(rgba(0x2828281a).into()), editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), diff --git a/crates/theme/src/themes/one.rs b/crates/theme/src/themes/one.rs index 3c8eb1085f0380bdc2df70acb6f5cb5472c88af0..5928939f7a5a51607f20cb7779ce35ad93a1278c 100644 --- a/crates/theme/src/themes/one.rs +++ b/crates/theme/src/themes/one.rs @@ -70,7 +70,7 @@ pub fn one() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x2f343eff).into()), editor_line_number: Some(rgba(0xc8ccd459).into()), editor_active_line_number: Some(rgba(0xc8ccd4ff).into()), - editor_invisible: Some(rgba(0x838994ff).into()), + editor_invisible: Some(rgba(0x555a63ff).into()), editor_wrap_guide: Some(rgba(0xc8ccd40d).into()), editor_active_wrap_guide: Some(rgba(0xc8ccd41a).into()), editor_document_highlight_read_background: Some(rgba(0x74ade81a).into()), @@ -521,7 +521,7 @@ pub fn one() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xebebecff).into()), editor_line_number: Some(rgba(0x383a4159).into()), editor_active_line_number: Some(rgba(0x383a41ff).into()), - editor_invisible: Some(rgba(0x7f8188ff).into()), + editor_invisible: Some(rgba(0xa3a3a4ff).into()), editor_wrap_guide: Some(rgba(0x383a410d).into()), editor_active_wrap_guide: Some(rgba(0x383a411a).into()), editor_document_highlight_read_background: Some(rgba(0x5c79e21a).into()), diff --git a/crates/theme/src/themes/rose_pine.rs b/crates/theme/src/themes/rose_pine.rs index fe3bddb2d0d6bdbf8d031001a6ef90359a102250..f654e4d9953cb5418eb3838b3ce27b8691a92913 100644 --- a/crates/theme/src/themes/rose_pine.rs +++ b/crates/theme/src/themes/rose_pine.rs @@ -70,7 +70,7 @@ pub fn rose_pine() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x1d1b2aff).into()), editor_line_number: Some(rgba(0xe0def459).into()), editor_active_line_number: Some(rgba(0xe0def4ff).into()), - editor_invisible: Some(rgba(0x75718eff).into()), + editor_invisible: Some(rgba(0x28253cff).into()), editor_wrap_guide: Some(rgba(0xe0def40d).into()), editor_active_wrap_guide: Some(rgba(0xe0def41a).into()), editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()), @@ -528,7 +528,7 @@ pub fn rose_pine() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xfef9f2ff).into()), editor_line_number: Some(rgba(0x57527959).into()), editor_active_line_number: Some(rgba(0x575279ff).into()), - editor_invisible: Some(rgba(0x706c8cff).into()), + editor_invisible: Some(rgba(0x9691a4ff).into()), editor_wrap_guide: Some(rgba(0x5752790d).into()), editor_active_wrap_guide: Some(rgba(0x5752791a).into()), editor_document_highlight_read_background: Some(rgba(0x57949f1a).into()), @@ -986,7 +986,7 @@ pub fn rose_pine() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x28253cff).into()), editor_line_number: Some(rgba(0xe0def459).into()), editor_active_line_number: Some(rgba(0xe0def4ff).into()), - editor_invisible: Some(rgba(0x85819eff).into()), + editor_invisible: Some(rgba(0x595571ff).into()), editor_wrap_guide: Some(rgba(0xe0def40d).into()), editor_active_wrap_guide: Some(rgba(0xe0def41a).into()), editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()), diff --git a/crates/theme/src/themes/sandcastle.rs b/crates/theme/src/themes/sandcastle.rs index bace9e936f8c26364cc57151b8bbc81c5cd21cc4..ccf49061014ccf4dbfe4c71393aa9d433e1f63f3 100644 --- a/crates/theme/src/themes/sandcastle.rs +++ b/crates/theme/src/themes/sandcastle.rs @@ -69,7 +69,7 @@ pub fn sandcastle() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x2b3039ff).into()), editor_line_number: Some(rgba(0xfdf4c159).into()), editor_active_line_number: Some(rgba(0xfdf4c1ff).into()), - editor_invisible: Some(rgba(0xa69782ff).into()), + editor_invisible: Some(rgba(0x7c6f64ff).into()), editor_wrap_guide: Some(rgba(0xfdf4c10d).into()), editor_active_wrap_guide: Some(rgba(0xfdf4c11a).into()), editor_document_highlight_read_background: Some(rgba(0x528b8b1a).into()), diff --git a/crates/theme/src/themes/solarized.rs b/crates/theme/src/themes/solarized.rs index 8d4d7e1aa1c253ed3e3236ea90f6bbe8b4fd5e00..b903d645396e78c9a2a3a1ebfe0c0f714b59e7d5 100644 --- a/crates/theme/src/themes/solarized.rs +++ b/crates/theme/src/themes/solarized.rs @@ -70,7 +70,7 @@ pub fn solarized() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x04313cff).into()), editor_line_number: Some(rgba(0xfdf6e359).into()), editor_active_line_number: Some(rgba(0xfdf6e3ff).into()), - editor_invisible: Some(rgba(0x93a1a1ff).into()), + editor_invisible: Some(rgba(0x6d8288ff).into()), editor_wrap_guide: Some(rgba(0xfdf6e30d).into()), editor_active_wrap_guide: Some(rgba(0xfdf6e31a).into()), editor_document_highlight_read_background: Some(rgba(0x288bd11a).into()), @@ -514,7 +514,7 @@ pub fn solarized() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0xf3eddaff).into()), editor_line_number: Some(rgba(0x002b3659).into()), editor_active_line_number: Some(rgba(0x002b36ff).into()), - editor_invisible: Some(rgba(0x34555eff).into()), + editor_invisible: Some(rgba(0x6d8288ff).into()), editor_wrap_guide: Some(rgba(0x002b360d).into()), editor_active_wrap_guide: Some(rgba(0x002b361a).into()), editor_document_highlight_read_background: Some(rgba(0x298bd11a).into()), diff --git a/crates/theme/src/themes/summercamp.rs b/crates/theme/src/themes/summercamp.rs index 2ee5f12394dd00328a2d2c698d44687aeeab94b6..0a580e6893578d846b475493897e294aafaf74eb 100644 --- a/crates/theme/src/themes/summercamp.rs +++ b/crates/theme/src/themes/summercamp.rs @@ -69,7 +69,7 @@ pub fn summercamp() -> UserThemeFamily { editor_highlighted_line_background: Some(rgba(0x231f16ff).into()), editor_line_number: Some(rgba(0xf8f5de59).into()), editor_active_line_number: Some(rgba(0xf8f5deff).into()), - editor_invisible: Some(rgba(0x736e55ff).into()), + editor_invisible: Some(rgba(0x494433ff).into()), editor_wrap_guide: Some(rgba(0xf8f5de0d).into()), editor_active_wrap_guide: Some(rgba(0xf8f5de1a).into()), editor_document_highlight_read_background: Some(rgba(0x499bef1a).into()), diff --git a/crates/theme_importer/src/zed1/converter.rs b/crates/theme_importer/src/zed1/converter.rs index 9f40c3695fcc6773e549d829e84a922aceef567c..2f640c799f99ddf21989890c2e7a37a2a332b791 100644 --- a/crates/theme_importer/src/zed1/converter.rs +++ b/crates/theme_importer/src/zed1/converter.rs @@ -240,7 +240,7 @@ impl Zed1ThemeConverter { editor_highlighted_line_background: convert(editor.highlighted_line_background), editor_line_number: convert(editor.line_number), editor_active_line_number: convert(editor.line_number_active), - editor_invisible: convert(highest.variant.default.foreground), // TODO: Is this light enough? + editor_invisible: convert(editor.whitespace), editor_wrap_guide: convert(editor.wrap_guide), editor_active_wrap_guide: convert(editor.active_wrap_guide), editor_document_highlight_read_background: convert( diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index e3ed076698d101a12f92c16048748532ea596e1c..e4057792796cad86d7fe31ff01b78ea9434ae102 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -111,7 +111,6 @@ mod test { let mut cx1 = VisualTestContext::from_window(cx.window, &cx); let editor1 = cx.editor.clone(); - dbg!(editor1.entity_id()); let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n")); let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx)); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dad7b50ca6c307aef780910ee03249633ccfbfda..aec33c2dd32ee9bda77e7fe7927e1346a86178df 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -6,6 +6,7 @@ use crate::{ }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; +use futures::{stream::FuturesUnordered, StreamExt}; use gpui::{ actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext, AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths, @@ -255,8 +256,8 @@ impl Pane { let focus_handle = cx.focus_handle(); let subscriptions = vec![ - cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)), - cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)), + cx.on_focus_in(&focus_handle, Pane::focus_in), + cx.on_focus_out(&focus_handle, Pane::focus_out), ]; let handle = cx.view().downgrade(); @@ -1796,23 +1797,46 @@ impl Pane { } } let mut to_pane = cx.view().clone(); - let split_direction = self.drag_split_direction; + let mut split_direction = self.drag_split_direction; let paths = paths.paths().to_vec(); self.workspace - .update(cx, |_, cx| { - cx.defer(move |workspace, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane(to_pane, split_direction, cx); + .update(cx, |workspace, cx| { + let fs = Arc::clone(workspace.project().read(cx).fs()); + cx.spawn(|workspace, mut cx| async move { + let mut is_file_checks = FuturesUnordered::new(); + for path in &paths { + is_file_checks.push(fs.is_file(path)) } - workspace - .open_paths( - paths, - OpenVisible::OnlyDirectories, - Some(to_pane.downgrade()), - cx, - ) - .detach(); - }); + let mut has_files_to_open = false; + while let Some(is_file) = is_file_checks.next().await { + if is_file { + has_files_to_open = true; + break; + } + } + drop(is_file_checks); + if !has_files_to_open { + split_direction = None; + } + + if let Some(open_task) = workspace + .update(&mut cx, |workspace, cx| { + if let Some(split_direction) = split_direction { + to_pane = workspace.split_pane(to_pane, split_direction, cx); + } + workspace.open_paths( + paths, + OpenVisible::OnlyDirectories, + Some(to_pane.downgrade()), + cx, + ) + }) + .ok() + { + let _opened_items: Vec<_> = open_task.await; + } + }) + .detach(); }) .log_err(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 839edf1009ab4c9397b16c9f9959bab89f8c169c..db8554712e68b091fa59d20f71c61e1824a5cf05 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -512,6 +512,11 @@ impl Workspace { project::Event::DisconnectedFromHost => { this.update_window_edited(cx); + let panes_to_unfollow: Vec> = + this.follower_states.keys().map(|k| k.clone()).collect(); + for pane in panes_to_unfollow { + this.unfollow(&pane, cx); + } cx.disable_focus(); } @@ -672,7 +677,7 @@ impl Workspace { // ); // this.show_notification(1, cx, |cx| { - // cx.build_view(|_cx| { + // cx.new_view(|_cx| { // simple_message_notification::MessageNotification::new(format!("Error:")) // .with_click_message("click here because!") // }) @@ -4363,12 +4368,15 @@ mod tests { use std::{cell::RefCell, rc::Rc}; use super::*; - use crate::item::{ - test::{TestItem, TestProjectItem}, - ItemEvent, + use crate::{ + dock::{test::TestPanel, PanelEvent}, + item::{ + test::{TestItem, TestProjectItem}, + ItemEvent, + }, }; use fs::FakeFs; - use gpui::TestAppContext; + use gpui::{px, DismissEvent, TestAppContext, VisualTestContext}; use project::{Project, ProjectEntryId}; use serde_json::json; use settings::SettingsStore; @@ -4935,362 +4943,405 @@ mod tests { }); } - // #[gpui::test] - // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { - // init_test(cx); - // let fs = FakeFs::new(cx.executor()); - - // let project = Project::test(fs, [], cx).await; - // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - - // let panel = workspace.update(cx, |workspace, cx| { - // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx)); - // workspace.add_panel(panel.clone(), cx); - - // workspace - // .right_dock() - // .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); - - // panel - // }); - - // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); - // pane.update(cx, |pane, cx| { - // let item = cx.build_view(|cx| TestItem::new(cx)); - // pane.add_item(Box::new(item), true, true, None, cx); - // }); - - // // Transfer focus from center to panel - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_panel_focus::(cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(!panel.is_zoomed(cx)); - // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Transfer focus from panel to center - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_panel_focus::(cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(!panel.is_zoomed(cx)); - // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Close the dock - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(!workspace.right_dock().read(cx).is_open()); - // assert!(!panel.is_zoomed(cx)); - // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Open the dock - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(!panel.is_zoomed(cx)); - // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Focus and zoom panel - // panel.update(cx, |panel, cx| { - // cx.focus_self(); - // panel.set_zoomed(true, cx) - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(panel.is_zoomed(cx)); - // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Transfer focus to the center closes the dock - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_panel_focus::(cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(!workspace.right_dock().read(cx).is_open()); - // assert!(panel.is_zoomed(cx)); - // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Transferring focus back to the panel keeps it zoomed - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_panel_focus::(cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(panel.is_zoomed(cx)); - // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Close the dock while it is zoomed - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx) - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(!workspace.right_dock().read(cx).is_open()); - // assert!(panel.is_zoomed(cx)); - // assert!(workspace.zoomed.is_none()); - // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Opening the dock, when it's zoomed, retains focus - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx) - // }); - - // workspace.update(cx, |workspace, cx| { - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(panel.is_zoomed(cx)); - // assert!(workspace.zoomed.is_some()); - // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); - // }); - - // // Unzoom and close the panel, zoom the active pane. - // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx) - // }); - // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); - - // // Opening a dock unzooms the pane. - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_dock(DockPosition::Right, cx) - // }); - // workspace.update(cx, |workspace, cx| { - // let pane = pane.read(cx); - // assert!(!pane.is_zoomed()); - // assert!(!pane.focus_handle(cx).is_focused(cx)); - // assert!(workspace.right_dock().read(cx).is_open()); - // assert!(workspace.zoomed.is_none()); - // }); - // } + #[gpui::test] + async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); - // #[gpui::test] - // async fn test_panels(cx: &mut gpui::TestAppContext) { - // init_test(cx); - // let fs = FakeFs::new(cx.executor()); - - // let project = Project::test(fs, [], cx).await; - // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - - // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { - // // Add panel_1 on the left, panel_2 on the right. - // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx)); - // workspace.add_panel(panel_1.clone(), cx); - // workspace - // .left_dock() - // .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); - // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx)); - // workspace.add_panel(panel_2.clone(), cx); - // workspace - // .right_dock() - // .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); - - // let left_dock = workspace.left_dock(); - // assert_eq!( - // left_dock.read(cx).visible_panel().unwrap().panel_id(), - // panel_1.panel_id() - // ); - // assert_eq!( - // left_dock.read(cx).active_panel_size(cx).unwrap(), - // panel_1.size(cx) - // ); - - // left_dock.update(cx, |left_dock, cx| { - // left_dock.resize_active_panel(Some(1337.), cx) - // }); - // assert_eq!( - // workspace - // .right_dock() - // .read(cx) - // .visible_panel() - // .unwrap() - // .panel_id(), - // panel_2.panel_id(), - // ); - - // (panel_1, panel_2) - // }); - - // // Move panel_1 to the right - // panel_1.update(cx, |panel_1, cx| { - // panel_1.set_position(DockPosition::Right, cx) - // }); - - // workspace.update(cx, |workspace, cx| { - // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. - // // Since it was the only panel on the left, the left dock should now be closed. - // assert!(!workspace.left_dock().read(cx).is_open()); - // assert!(workspace.left_dock().read(cx).visible_panel().is_none()); - // let right_dock = workspace.right_dock(); - // assert_eq!( - // right_dock.read(cx).visible_panel().unwrap().panel_id(), - // panel_1.panel_id() - // ); - // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); - - // // Now we move panel_2 to the left - // panel_2.set_position(DockPosition::Left, cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // // Since panel_2 was not visible on the right, we don't open the left dock. - // assert!(!workspace.left_dock().read(cx).is_open()); - // // And the right dock is unaffected in it's displaying of panel_1 - // assert!(workspace.right_dock().read(cx).is_open()); - // assert_eq!( - // workspace - // .right_dock() - // .read(cx) - // .visible_panel() - // .unwrap() - // .panel_id(), - // panel_1.panel_id(), - // ); - // }); - - // // Move panel_1 back to the left - // panel_1.update(cx, |panel_1, cx| { - // panel_1.set_position(DockPosition::Left, cx) - // }); - - // workspace.update(cx, |workspace, cx| { - // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. - // let left_dock = workspace.left_dock(); - // assert!(left_dock.read(cx).is_open()); - // assert_eq!( - // left_dock.read(cx).visible_panel().unwrap().panel_id(), - // panel_1.panel_id() - // ); - // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); - // // And right the dock should be closed as it no longer has any panels. - // assert!(!workspace.right_dock().read(cx).is_open()); - - // // Now we move panel_1 to the bottom - // panel_1.set_position(DockPosition::Bottom, cx); - // }); - - // workspace.update(cx, |workspace, cx| { - // // Since panel_1 was visible on the left, we close the left dock. - // assert!(!workspace.left_dock().read(cx).is_open()); - // // The bottom dock is sized based on the panel's default size, - // // since the panel orientation changed from vertical to horizontal. - // let bottom_dock = workspace.bottom_dock(); - // assert_eq!( - // bottom_dock.read(cx).active_panel_size(cx).unwrap(), - // panel_1.size(cx), - // ); - // // Close bottom dock and move panel_1 back to the left. - // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); - // panel_1.set_position(DockPosition::Left, cx); - // }); - - // // Emit activated event on panel 1 - // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate)); - - // // Now the left dock is open and panel_1 is active and focused. - // workspace.update(cx, |workspace, cx| { - // let left_dock = workspace.left_dock(); - // assert!(left_dock.read(cx).is_open()); - // assert_eq!( - // left_dock.read(cx).visible_panel().unwrap().panel_id(), - // panel_1.panel_id(), - // ); - // assert!(panel_1.focus_handle(cx).is_focused(cx)); - // }); - - // // Emit closed event on panel 2, which is not active - // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // // Wo don't close the left dock, because panel_2 wasn't the active panel - // workspace.update(cx, |workspace, cx| { - // let left_dock = workspace.left_dock(); - // assert!(left_dock.read(cx).is_open()); - // assert_eq!( - // left_dock.read(cx).visible_panel().unwrap().panel_id(), - // panel_1.panel_id(), - // ); - // }); - - // // Emitting a ZoomIn event shows the panel as zoomed. - // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn)); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); - // }); - - // // Move panel to another dock while it is zoomed - // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - - // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - // }); - - // // If focus is transferred to another view that's not a panel or another pane, we still show - // // the panel as zoomed. - // let other_focus_handle = cx.update(|cx| cx.focus_handle()); - // cx.update(|cx| cx.focus(&other_focus_handle)); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - // }); - - // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. - // workspace.update(cx, |_, cx| cx.focus_self()); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, None); - // assert_eq!(workspace.zoomed_position, None); - // }); - - // // If focus is transferred again to another view that's not a panel or a pane, we won't - // // show the panel as zoomed because it wasn't zoomed before. - // cx.update(|cx| cx.focus(&other_focus_handle)); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, None); - // assert_eq!(workspace.zoomed_position, None); - // }); - - // // When focus is transferred back to the panel, it is zoomed again. - // panel_1.update(cx, |_, cx| cx.focus_self()); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - // }); - - // // Emitting a ZoomOut event unzooms the panel. - // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut)); - // workspace.update(cx, |workspace, _| { - // assert_eq!(workspace.zoomed, None); - // assert_eq!(workspace.zoomed_position, None); - // }); - - // // Emit closed event on panel 1, which is active - // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // // Now the left dock is closed, because panel_1 was the active panel - // workspace.update(cx, |workspace, cx| { - // let right_dock = workspace.right_dock(); - // assert!(!right_dock.read(cx).is_open()); - // }); - // } + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + let panel = workspace.update(cx, |workspace, cx| { + let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx)); + workspace.add_panel(panel.clone(), cx); + + workspace + .right_dock() + .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + panel + }); + + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + pane.update(cx, |pane, cx| { + let item = cx.new_view(|cx| TestItem::new(cx)); + pane.add_item(Box::new(item), true, true, None, cx); + }); + + // Transfer focus from center to panel + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Transfer focus from panel to center + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Close the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Open the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Focus and zoom panel + panel.update(cx, |panel, cx| { + cx.focus_self(); + panel.set_zoomed(true, cx) + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Transfer focus to the center closes the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Transferring focus back to the panel keeps it zoomed + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Close the dock while it is zoomed + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + + workspace.update(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(workspace.zoomed.is_none()); + assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Opening the dock, when it's zoomed, retains focus + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(workspace.zoomed.is_some()); + assert!(panel.read(cx).focus_handle(cx).contains_focused(cx)); + }); + + // Unzoom and close the panel, zoom the active pane. + panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + + // Opening a dock unzooms the pane. + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + workspace.update(cx, |workspace, cx| { + let pane = pane.read(cx); + assert!(!pane.is_zoomed()); + assert!(!pane.focus_handle(cx).is_focused(cx)); + assert!(workspace.right_dock().read(cx).is_open()); + assert!(workspace.zoomed.is_none()); + }); + } + + struct TestModal(FocusHandle); + + impl TestModal { + fn new(cx: &mut ViewContext) -> Self { + Self(cx.focus_handle()) + } + } + + impl EventEmitter for TestModal {} + + impl FocusableView for TestModal { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.0.clone() + } + } + + impl ModalView for TestModal {} + + impl Render for TestModal { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().track_focus(&self.0) + } + } + + #[gpui::test] + async fn test_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { + let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace.add_panel(panel_1.clone(), cx); + workspace + .left_dock() + .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); + let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx)); + workspace.add_panel(panel_2.clone(), cx); + workspace + .right_dock() + .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + let left_dock = workspace.left_dock(); + assert_eq!( + left_dock.read(cx).visible_panel().unwrap().panel_id(), + panel_1.panel_id() + ); + assert_eq!( + left_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx) + ); + + left_dock.update(cx, |left_dock, cx| { + left_dock.resize_active_panel(Some(px(1337.)), cx) + }); + assert_eq!( + workspace + .right_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + panel_2.panel_id(), + ); + + (panel_1, panel_2) + }); + + // Move panel_1 to the right + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Right, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. + // Since it was the only panel on the left, the left dock should now be closed. + assert!(!workspace.left_dock().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).visible_panel().is_none()); + let right_dock = workspace.right_dock(); + assert_eq!( + right_dock.read(cx).visible_panel().unwrap().panel_id(), + panel_1.panel_id() + ); + assert_eq!( + right_dock.read(cx).active_panel_size(cx).unwrap(), + px(1337.) + ); + + // Now we move panel_2 to the left + panel_2.set_position(DockPosition::Left, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_2 was not visible on the right, we don't open the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // And the right dock is unaffected in it's displaying of panel_1 + assert!(workspace.right_dock().read(cx).is_open()); + assert_eq!( + workspace + .right_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + panel_1.panel_id(), + ); + }); + + // Move panel_1 back to the left + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Left, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).visible_panel().unwrap().panel_id(), + panel_1.panel_id() + ); + assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.)); + // And the right dock should be closed as it no longer has any panels. + assert!(!workspace.right_dock().read(cx).is_open()); + + // Now we move panel_1 to the bottom + panel_1.set_position(DockPosition::Bottom, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, we close the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // The bottom dock is sized based on the panel's default size, + // since the panel orientation changed from vertical to horizontal. + let bottom_dock = workspace.bottom_dock(); + assert_eq!( + bottom_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx), + ); + // Close bottom dock and move panel_1 back to the left. + bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); + panel_1.set_position(DockPosition::Left, cx); + }); + + // Emit activated event on panel 1 + panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate)); + + // Now the left dock is open and panel_1 is active and focused. + workspace.update(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).visible_panel().unwrap().panel_id(), + panel_1.panel_id(), + ); + assert!(panel_1.focus_handle(cx).is_focused(cx)); + }); + + // Emit closed event on panel 2, which is not active + panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close)); + + // Wo don't close the left dock, because panel_2 wasn't the active panel + workspace.update(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).visible_panel().unwrap().panel_id(), + panel_1.panel_id(), + ); + }); + + // Emitting a ZoomIn event shows the panel as zoomed. + panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn)); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); + }); + + // Move panel to another dock while it is zoomed + panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); + + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + }); + + // This is a helper for getting a: + // - valid focus on an element, + // - that isn't a part of the panes and panels system of the Workspace, + // - and doesn't trigger the 'on_focus_lost' API. + let focus_other_view = { + let workspace = workspace.clone(); + move |cx: &mut VisualTestContext| { + workspace.update(cx, |workspace, cx| { + if let Some(_) = workspace.active_modal::(cx) { + workspace.toggle_modal(cx, TestModal::new); + workspace.toggle_modal(cx, TestModal::new); + } else { + workspace.toggle_modal(cx, TestModal::new); + } + }) + } + }; + + // If focus is transferred to another view that's not a panel or another pane, we still show + // the panel as zoomed. + focus_other_view(cx); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + }); + + // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. + workspace.update(cx, |_, cx| cx.focus_self()); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); + }); + + // If focus is transferred again to another view that's not a panel or a pane, we won't + // show the panel as zoomed because it wasn't zoomed before. + focus_other_view(cx); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); + }); + + // When the panel is activated, it is zoomed again. + cx.dispatch_action(ToggleRightDock); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + }); + + // Emitting a ZoomOut event unzooms the panel. + panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut)); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); + }); + + // Emit closed event on panel 1, which is active + panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close)); + + // Now the left dock is closed, because panel_1 was the active panel + workspace.update(cx, |workspace, cx| { + let right_dock = workspace.right_dock(); + assert!(!right_dock.read(cx).is_open()); + }); + } pub fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 38e0bec14e30418bee589450bc21cccc31364daf..c2725eef64029a11cbf449769ee5f025ae7b0535 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -148,6 +148,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.on_window_should_close(move |cx| { handle .update(cx, |workspace, cx| { + // We'll handle closing asynchoronously workspace.close_window(&Default::default(), cx); false }) diff --git a/docs/old/tools.md b/docs/old/tools.md index 22810e3e07909c6d0f42bdd4c2336d8cd438bf7a..56c3c0c963278eb2b757d642e7854391621d20f3 100644 --- a/docs/old/tools.md +++ b/docs/old/tools.md @@ -56,7 +56,7 @@ We use Vercel for all of our web deployments and some backend things. If you sig ### Environment Variables -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. +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. 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). diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 79d2d20a7afd81fc00eb1e142d78473562b7f335..5cdc76def26ae769f69b0f69555ec5663fcf1937 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,5 @@ [toolchain] channel = "1.75" -components = [ "rustfmt" ] +profile = "minimal" +components = [ "rustfmt", "clippy" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] diff --git a/script/zed-local b/script/zed-local index 8ba1561bbda31482191945f018c6a29befb12925..4519ede38cf06bab355d0e65c530b76426e9108c 100755 --- a/script/zed-local +++ b/script/zed-local @@ -4,20 +4,28 @@ const { spawn, execFileSync } = require("child_process"); const RESOLUTION_REGEX = /(\d+) x (\d+)/; const DIGIT_FLAG_REGEX = /^--?(\d+)$/; -const RELEASE_MODE = "--release"; - -const args = process.argv.slice(2); // Parse the number of Zed instances to spawn. let instanceCount = 1; -const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX); -if (digitMatch) { - instanceCount = parseInt(digitMatch[1]); - args.shift(); -} -const isReleaseMode = args.some((arg) => arg === RELEASE_MODE); -if (instanceCount > 4) { - throw new Error("Cannot spawn more than 4 instances"); +let isReleaseMode = false; +let isTop = false; + +const args = process.argv.slice(2); +for (const arg of args) { + const digitMatch = arg.match(DIGIT_FLAG_REGEX); + if (digitMatch) { + instanceCount = parseInt(digitMatch[1]); + continue; + } + + if (arg == "--release") { + isReleaseMode = true; + continue; + } + + if (arg == "--top") { + isTop = true; + } } // Parse the resolution of the main screen @@ -34,7 +42,11 @@ if (!mainDisplayResolution) { throw new Error("Could not parse screen resolution"); } const screenWidth = parseInt(mainDisplayResolution[1]); -const screenHeight = parseInt(mainDisplayResolution[2]); +let screenHeight = parseInt(mainDisplayResolution[2]); + +if (isTop) { + screenHeight = Math.floor(screenHeight / 2); +} // Determine the window size for each instance let instanceWidth = screenWidth;