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

# Conflicts:
#	crates/gpui/src/window.rs
This commit is contained in:
Antonio Scandurra 2024-01-12 14:31:13 +01:00
commit 94293b3bf9
73 changed files with 2531 additions and 1824 deletions

View file

@ -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

17
.github/actions/check_style/action.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -1,5 +1,6 @@
{
"JSON": {
"tab_size": 4
}
},
"formatter": "auto"
}

70
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -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

View file

@ -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")));

View file

@ -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,11 +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 mut updates = room.updates();
while let Some(update) = updates.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
@ -143,26 +140,7 @@ impl Room {
};
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 this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
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<Self>,
) -> 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<Self>,
) -> 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 {

View file

@ -1371,10 +1371,7 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
})
}
async fn write_credentials_to_keychain(
credentials: Credentials,
cx: &AsyncAppContext,
) -> Result<()> {
fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext) -> Result<()> {
cx.update(move |cx| {
cx.write_credentials(
&ZED_SERVER_URL,
@ -1384,7 +1381,7 @@ async fn write_credentials_to_keychain(
})?
}
async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
}

View file

@ -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<dyn HttpClient>,
executor: BackgroundExecutor,
@ -34,6 +37,7 @@ struct TelemetryState {
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
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<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
@ -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::<SettingsStore>({
@ -194,7 +204,7 @@ impl Telemetry {
#[cfg(not(any(test, feature = "test-support")))]
fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
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<Self>, 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<Self>, 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

View file

@ -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<Utc>,
end: Option<DateTime<Utc>>,
}
pub struct EventCoalescer {
state: Option<PeriodData>,
}
impl EventCoalescer {
pub fn new() -> Self {
Self { state: None }
}
pub fn log_event(
&mut self,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
self.log_event_with_time(Utc::now(), environment)
}
// pub fn close_current_period(&mut self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
// self.environment.map(|env| self.log_event(env)).flatten()
// }
fn log_event_with_time(
&mut self,
log_time: DateTime<Utc>,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'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
}

View file

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

View file

@ -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");

View file

@ -0,0 +1 @@
ALTER TABLE rooms ADD COLUMN environment TEXT;

View file

@ -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)

View file

@ -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<RoomGuard<JoinRoom>> {
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<ChannelId>, Option<String>) =
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))?;
}
}

View file

@ -8,7 +8,7 @@ pub struct Model {
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
pub enviroment: Option<String>,
pub environment: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -599,152 +599,6 @@ async fn test_channel_buffers_and_server_restarts(
});
}
#[gpui::test(iterations = 10)]
async fn test_following_to_channel_notes_without_a_shared_project(
deterministic: BackgroundExecutor,
mut cx_a: &mut TestAppContext,
mut cx_b: &mut TestAppContext,
mut cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.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 client_c = server.create_client(cx_c, "user_c").await;
cx_a.update(editor::init);
cx_b.update(editor::init);
cx_c.update(editor::init);
cx_a.update(collab_ui::channel_view::init);
cx_b.update(collab_ui::channel_view::init);
cx_c.update(collab_ui::channel_view::init);
let channel_1_id = server
.make_channel(
"channel-1",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let channel_2_id = server
.make_channel(
"channel-2",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
// Clients A, B, and C join a channel.
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let active_call_c = cx_c.read(ActiveCall::global);
for (call, cx) in [
(&active_call_a, &mut cx_a),
(&active_call_b, &mut cx_b),
(&active_call_c, &mut cx_c),
] {
call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
.await
.unwrap();
}
deterministic.run_until_parked();
// Clients A, B, and C all open their own unshared projects.
client_a.fs().insert_tree("/a", json!({})).await;
client_b.fs().insert_tree("/b", json!({})).await;
client_c.fs().insert_tree("/c", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
// Client A opens the notes for channel 1.
let channel_view_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_view_1_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
});
// Client B follows client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.start_following(client_a.peer_id().unwrap(), cx)
.unwrap()
})
.await
.unwrap();
// Client B is taken to the notes for channel 1, with the same
// text selected as client A.
deterministic.run_until_parked();
let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_view_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
});
// Client A opens the notes for channel 2.
eprintln!("opening -------------------->");
let channel_view_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_view_2_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
});
// Client B is taken to the notes for channel 2.
deterministic.run_until_parked();
eprintln!("opening <--------------------");
let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_view_2_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
});
}
#[gpui::test]
async fn test_channel_buffer_changes(
deterministic: BackgroundExecutor,

View file

@ -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

View file

@ -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<ChannelChat>, 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;
#[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;
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();
// 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();
channel_chat_a
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
.await
.unwrap();
// executor.run_until_parked();
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()
// });
let b_has_messages = cx_b.update(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
// assert!(b_has_messages);
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();
// 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);
// executor.run_until_parked();
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();
// let b_has_messages = cx_b.read_with(|cx| {
// client_b
// .channel_store()
// .read(cx)
// .has_new_messages(channel_id)
// .unwrap()
// });
executor.run_until_parked();
// assert!(!b_has_messages);
let b_has_messages = cx_b.update(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
// // 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();
assert!(!b_has_messages);
// executor.run_until_parked();
// 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();
// let b_has_messages = cx_b.read_with(|cx| {
// client_b
// .channel_store()
// .read(cx)
// .has_new_messages(channel_id)
// .unwrap()
// });
executor.run_until_parked();
// assert!(!b_has_messages);
let b_has_messages = cx_b.update(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
// // 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);
// });
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("three".into(), cx).unwrap())
// .await
// .unwrap();
// 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);
});
// executor.run_until_parked();
// 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();
// let b_has_messages = cx_b.read_with(|cx| {
// client_b
// .channel_store()
// .read(cx)
// .has_new_messages(channel_id)
// .unwrap()
// });
executor.run_until_parked();
// assert!(b_has_messages);
let b_has_messages = cx_b.update(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
// // Closing the chat should re-enable change tracking
// cx_b.update(|_| drop(chat_panel_b));
assert!(b_has_messages);
// channel_chat_a
// .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
// .await
// .unwrap();
// Closing the chat should re-enable change tracking
cx_b.update(|_| drop(chat_panel_b));
// executor.run_until_parked();
channel_chat_a
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
.await
.unwrap();
// let b_has_messages = cx_b.read_with(|cx| {
// client_b
// .channel_store()
// .read(cx)
// .has_new_messages(channel_id)
// .unwrap()
// });
executor.run_until_parked();
// assert!(b_has_messages);
// }
let b_has_messages = cx_b.update(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
}

View file

@ -1,9 +1,12 @@
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use call::ActiveCall;
use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
use call::{ActiveCall, ParticipantLocation};
use collab_ui::{
channel_view::ChannelView,
notifications::project_shared_notification::ProjectSharedNotification,
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext,
point, BackgroundExecutor, Context, Entity, SharedString, TestAppContext, View, VisualContext,
VisualTestContext,
};
use language::Capability;
@ -1568,6 +1571,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::<Editor>(cx).unwrap()
});
let editor_b = workspace_b.update(cx_b, |workspace, cx| {
workspace.active_item_as::<Editor>(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 +1649,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(
@ -1772,3 +1825,167 @@ fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Ve
.collect()
})
}
#[gpui::test(iterations = 10)]
async fn test_following_to_channel_notes_without_a_shared_project(
deterministic: BackgroundExecutor,
mut cx_a: &mut TestAppContext,
mut cx_b: &mut TestAppContext,
mut cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.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 client_c = server.create_client(cx_c, "user_c").await;
cx_a.update(editor::init);
cx_b.update(editor::init);
cx_c.update(editor::init);
cx_a.update(collab_ui::channel_view::init);
cx_b.update(collab_ui::channel_view::init);
cx_c.update(collab_ui::channel_view::init);
let channel_1_id = server
.make_channel(
"channel-1",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let channel_2_id = server
.make_channel(
"channel-2",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
// Clients A, B, and C join a channel.
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let active_call_c = cx_c.read(ActiveCall::global);
for (call, cx) in [
(&active_call_a, &mut cx_a),
(&active_call_b, &mut cx_b),
(&active_call_c, &mut cx_c),
] {
call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
.await
.unwrap();
}
deterministic.run_until_parked();
// Clients A, B, and C all open their own unshared projects.
client_a
.fs()
.insert_tree("/a", json!({ "1.txt": "" }))
.await;
client_b.fs().insert_tree("/b", json!({})).await;
client_c.fs().insert_tree("/c", json!({})).await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
// Client A opens the notes for channel 1.
let channel_notes_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
});
// Client B follows client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.start_following(client_a.peer_id().unwrap(), cx)
.unwrap()
})
.await
.unwrap();
// Client B is taken to the notes for channel 1, with the same
// text selected as client A.
deterministic.run_until_parked();
let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
});
// Client A opens the notes for channel 2.
let channel_notes_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
});
// Client B is taken to the notes for channel 2.
deterministic.run_until_parked();
let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_notes_2_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
});
// Client A opens a local buffer in their unshared project.
let _unshared_editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
// This does not send any leader update message to client B.
// If it did, an error would occur on client B, since this buffer
// is not shared with them.
deterministic.run_until_parked();
workspace_b.update(cx_b, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).expect("no active item").item_id(),
channel_notes_2_b.entity_id()
);
});
}

View file

@ -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<Workspace>,
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<Workspace>, &'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<Project> {
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<Workspace>, &'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,

View file

@ -111,7 +111,6 @@ fn notification_window_options(
let screen_bounds = screen.bounds();
let size: Size<GlobalPixels> = 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::<GlobalPixels> {
origin: screen_bounds.upper_right()
- point(

View file

@ -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<u32>;
@ -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);

View file

@ -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),

View file

@ -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 {
@ -2258,11 +2269,9 @@ impl EditorElement {
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| {
cx.listener_for(&self.editor, move |editor, _, cx| {
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
});
jump_handler
})
});
let element = if *starts_new_buffer {
@ -2342,34 +2351,25 @@ impl EditorElement {
.text_color(cx.theme().colors().editor_line_number)
.child("..."),
)
.map(|this| {
if let Some(jump_handler) = jump_handler {
this.child(
ButtonLike::new("jump to collapsed context")
.style(ButtonStyle::Transparent)
.full_width()
.on_click(jump_handler)
.tooltip(|cx| {
Tooltip::for_action(
"Jump to Buffer",
&OpenExcerpts,
cx,
)
})
.child(
div()
.h_px()
.w_full()
.bg(cx.theme().colors().border_variant)
.group_hover("", |style| {
style.bg(cx.theme().colors().border)
}),
),
.child(
ButtonLike::new("jump to collapsed context")
.style(ButtonStyle::Transparent)
.full_width()
.child(
div()
.h_px()
.w_full()
.bg(cx.theme().colors().border_variant)
.group_hover("", |style| {
style.bg(cx.theme().colors().border)
}),
)
} else {
this.child(div().size_full().bg(gpui::green()))
}
})
.when_some(jump_handler, |this, jump_handler| {
this.on_click(jump_handler).tooltip(|cx| {
Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
})
}),
)
};
element.into_any()
}

View file

@ -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<Editor>) {
let head = editor.selections.newest_display(cx).head();

View file

@ -82,7 +82,9 @@ impl FollowableItem for Editor {
let pane = pane.downgrade();
Some(cx.spawn(|mut cx| async move {
let mut buffers = futures::future::try_join_all(buffers).await?;
let mut buffers = futures::future::try_join_all(buffers)
.await
.debug_assert_ok("leaders don't share views for unshared buffers")?;
let editor = pane.update(&mut cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {

View file

@ -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. :)

View file

@ -23,6 +23,7 @@ theme = { path = "../theme" }
ui = { path = "../ui" }
workspace = { path = "../workspace" }
postage.workspace = true
anyhow.workspace = true
serde.workspace = true
[dev-dependencies]

View file

@ -312,7 +312,7 @@ impl FileFinderDelegate {
cx: &mut ViewContext<FileFinder>,
) -> 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<FileSearchQuery>,
cx: &mut ViewContext<'_, Picker<Self>>,
) -> 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::<Editor>(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::<PathBuf>::new(),
"Mismatching abs path should produce no matches"
)
});
}
#[gpui::test]
async fn test_complex_path(cx: &mut TestAppContext) {
let app_state = init_test(cx);

View file

@ -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;

View file

@ -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<Vec<Box<dyn Action>>> {
match self {
KeyMatch::Some(matches) => Some(matches),
_ => None,
}
}
}
// #[cfg(test)]
// mod tests {
// use anyhow::Result;
// use serde::Deserialize;
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;
}
// use crate::{actions, impl_actions, keymap_matcher::ActionContext};
for (a, b) in a.iter().zip(b.iter()) {
if !a.partial_eq(b.as_ref()) {
return false;
}
}
// use super::*;
true
}
_ => false,
}
}
}
// #[test]
// fn test_keymap_and_view_ordering() -> Result<()> {
// actions!(test, [EditorAction, ProjectPanelAction]);
#[cfg(test)]
mod tests {
// let mut editor = ActionContext::default();
// editor.add_identifier("Editor");
use serde_derive::Deserialize;
// let mut project_panel = ActionContext::default();
// project_panel.add_identifier("ProjectPanel");
use super::*;
use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
use crate::{actions, KeyBinding};
// // Editor 'deeper' in than project panel
// let dispatch_path = vec![(2, editor), (1, project_panel)];
#[test]
fn test_keymap_and_view_ordering() {
actions!(test, [EditorAction, ProjectPanelAction]);
// // 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 editor = KeyContext::default();
editor.add("Editor");
// let mut matcher = KeymapMatcher::new(keymap);
let mut project_panel = KeyContext::default();
project_panel.add("ProjectPanel");
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![
// (2, Box::new(EditorAction)),
// (1, Box::new(ProjectPanelAction)),
// ]),
// );
// Editor 'deeper' in than project panel
let dispatch_path = vec![project_panel, editor];
// Ok(())
// }
// But editor actions 'higher' up in keymap
let keymap = Keymap::new(vec![
KeyBinding::new("left", EditorAction, Some("Editor")),
KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
]);
// #[test]
// fn test_push_keystroke() -> Result<()> {
// actions!(test, [B, AB, C, D, DA, E, EF]);
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
// let mut context1 = ActionContext::default();
// context1.add_identifier("1");
let matches = matcher
.match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
.matches()
.unwrap();
// let mut context2 = ActionContext::default();
// context2.add_identifier("2");
assert!(matches[0].partial_eq(&EditorAction));
assert!(matches.get(1).is_none());
}
// let dispatch_path = vec![(2, context2), (1, context1)];
#[test]
fn test_multi_keystroke_match() {
actions!(test, [B, AB, C, D, DA, E, EF]);
// 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 context1 = KeyContext::default();
context1.add("1");
// let mut matcher = KeymapMatcher::new(keymap);
let mut context2 = KeyContext::default();
context2.add("2");
// // 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());
let dispatch_path = vec![context2, context1];
// // 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());
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")),
]);
// // 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());
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
// // 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))]),
// );
// 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());
// // 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());
// 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());
// Ok(())
// }
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());
// #[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,
// }
// );
// 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)]),
);
}
// assert_eq!(
// Keystroke::parse("alt-shift-down")?,
// Keystroke {
// key: "down".into(),
// ctrl: false,
// alt: true,
// shift: true,
// cmd: false,
// function: false,
// ime_key: None,
// }
// );
#[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("shift-cmd--")?,
// Keystroke {
// key: "-".into(),
// ctrl: false,
// alt: false,
// shift: true,
// cmd: true,
// 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,
}
);
// Ok(())
// }
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() -> Result<()> {
// use KeymapContextPredicate::*;
#[test]
fn test_context_predicate_parsing() {
use KeyBindingContextPredicate::*;
// 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!(
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!(
// KeymapContextPredicate::parse("!a")?,
// Not(Box::new(Identifier("a".into())),)
// );
assert_eq!(
KeyBindingContextPredicate::parse("!a").unwrap(),
Not(Box::new(Identifier("a".into())),)
);
}
// Ok(())
// }
#[test]
fn test_context_predicate_eval() {
let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
// #[test]
// fn test_context_predicate_eval() {
// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeyContext::default();
context.add("a");
assert!(!predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// assert!(!predicate.eval(&[context]));
let mut context = KeyContext::default();
context.add("a");
context.add("b");
assert!(predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// context.add_identifier("b");
// assert!(predicate.eval(&[context]));
let mut context = KeyContext::default();
context.add("a");
context.set("c", "x");
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 = KeyContext::default();
context.add("a");
context.set("c", "d");
assert!(predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// context.add_key("c", "d");
// assert!(predicate.eval(&[context]));
let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
assert!(predicate.eval(&[KeyContext::default()]));
}
// let predicate = KeymapContextPredicate::parse("!a").unwrap();
// assert!(predicate.eval(&[ActionContext::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"]),
];
// #[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]));
// 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"]),
];
// 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]));
// 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
}
}
// 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() {
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct A(pub String);
impl_actions!(test, [A]);
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
// #[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,
}
// #[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 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 = KeyContext::default();
context_a.add("a");
// let mut context_a = ActionContext::default();
// context_a.add_identifier("a");
let mut context_b = KeyContext::default();
context_b.add("b");
// let mut context_b = ActionContext::default();
// context_b.add_identifier("b");
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
// let mut matcher = KeymapMatcher::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();
// // 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").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();
// // 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").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();
// // 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();
let mut context_c = KeyContext::default();
context_c.add("c");
// // 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();
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)])
);
// let mut context_c = ActionContext::default();
// context_c.add_identifier("c");
// handle Czech $ (option + 4 key)
assert_eq!(
matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]),
KeyMatch::Some(vec![Box::new(Dollar)])
);
// // 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 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 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 ctrl+` on a brazillian keyboard
assert_eq!(
matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
KeyMatch::Some(vec![Box::new(Backtick)])
);
// // 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(())
// }
// }
// 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)])
);
}
}

View file

@ -978,8 +978,12 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
unsafe {
if let Some(event) = InputEvent::from_native(native_event, None) {
let platform = get_mac_platform(this);
if let Some(callback) = platform.0.lock().event.as_mut() {
if !callback(event) {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.event.take() {
drop(lock);
let result = callback(event);
platform.0.lock().event.get_or_insert(callback);
if !result {
return;
}
}
@ -1004,30 +1008,42 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
if !has_open_windows {
let platform = unsafe { get_mac_platform(this) };
if let Some(callback) = platform.0.lock().reopen.as_mut() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.reopen.take() {
drop(lock);
callback();
platform.0.lock().reopen.get_or_insert(callback);
}
}
}
extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
if let Some(callback) = platform.0.lock().become_active.as_mut() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.become_active.take() {
drop(lock);
callback();
platform.0.lock().become_active.get_or_insert(callback);
}
}
extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
if let Some(callback) = platform.0.lock().resign_active.as_mut() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.resign_active.take() {
drop(lock);
callback();
platform.0.lock().resign_active.get_or_insert(callback);
}
}
extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
if let Some(callback) = platform.0.lock().quit.as_mut() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.quit.take() {
drop(lock);
callback();
platform.0.lock().quit.get_or_insert(callback);
}
}
@ -1047,22 +1063,27 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
.collect::<Vec<_>>()
};
let platform = unsafe { get_mac_platform(this) };
if let Some(callback) = platform.0.lock().open_urls.as_mut() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.open_urls.take() {
drop(lock);
callback(urls);
platform.0.lock().open_urls.get_or_insert(callback);
}
}
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
unsafe {
let platform = get_mac_platform(this);
let mut platform = platform.0.lock();
if let Some(mut callback) = platform.menu_command.take() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.menu_command.take() {
let tag: NSInteger = msg_send![item, tag];
let index = tag as usize;
if let Some(action) = platform.menu_actions.get(index) {
callback(action.as_ref());
if let Some(action) = lock.menu_actions.get(index) {
let action = action.boxed_clone();
drop(lock);
callback(&*action);
}
platform.menu_command = Some(callback);
platform.0.lock().menu_command.get_or_insert(callback);
}
}
}
@ -1071,14 +1092,20 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
unsafe {
let mut result = false;
let platform = get_mac_platform(this);
let mut platform = platform.0.lock();
if let Some(mut callback) = platform.validate_menu_command.take() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.validate_menu_command.take() {
let tag: NSInteger = msg_send![item, tag];
let index = tag as usize;
if let Some(action) = platform.menu_actions.get(index) {
if let Some(action) = lock.menu_actions.get(index) {
let action = action.boxed_clone();
drop(lock);
result = callback(action.as_ref());
}
platform.validate_menu_command = Some(callback);
platform
.0
.lock()
.validate_menu_command
.get_or_insert(callback);
}
result
}
@ -1087,10 +1114,11 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
unsafe {
let platform = get_mac_platform(this);
let mut platform = platform.0.lock();
if let Some(mut callback) = platform.will_open_menu.take() {
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.will_open_menu.take() {
drop(lock);
callback();
platform.will_open_menu = Some(callback);
platform.0.lock().will_open_menu.get_or_insert(callback);
}
}
}

View file

@ -190,6 +190,9 @@ impl MacTextSystemState {
for font in family.fonts() {
let mut font = font.load()?;
open_type::apply_features(&mut font, features);
let Some(_) = font.glyph_for_char('m') else {
continue;
};
let font_id = FontId(self.fonts.len());
font_ids.push(font_id);
let postscript_name = font.postscript_name().unwrap();
@ -592,169 +595,49 @@ impl From<FontStyle> for FontkitStyle {
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::AppContext;
// use font_kit::properties::{Style, Weight};
// use platform::FontSystem as _;
#[cfg(test)]
mod tests {
use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
// #[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);
#[test]
fn test_wrap_line() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
// 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);
// }
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()]);
// #[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 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(),]
);
}
// 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<_>>(),
// 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]
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(),
};
// #[test]
// #[ignore]
// fn test_rasterize_glyph() {
// use std::{fs::File, io::BufWriter, path::Path};
let layout = fonts.layout_line(line, px(16.), &[style]);
assert_eq!(layout.len, line.len());
assert!(layout.runs.is_empty());
// 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
// }
// }
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
}
}

View file

@ -268,6 +268,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(
@ -683,9 +684,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();
}
})
@ -1104,37 +1102,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 <quote><space>
// - ctrl-` on a brazillian layout by typing <ctrl-`>
// - $ on a czech QWERTY layout by typing <alt-4>
// - 4 on a czech QWERTY layout by typing <shift-4>
// - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
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 {
@ -1574,6 +1542,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);
}
}

View file

@ -65,6 +65,9 @@ impl TextSystem {
}
}
pub fn all_font_families(&self) -> Vec<String> {
self.platform_text_system.all_font_families()
}
pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
self.platform_text_system.add_fonts(fonts)
}
@ -368,28 +371,20 @@ impl TextSystem {
self.line_layout_cache.finish_frame(reused_views)
}
pub fn line_wrapper(
self: &Arc<Self>,
font: Font,
font_size: Pixels,
) -> Result<LineWrapperHandle> {
pub fn line_wrapper(self: &Arc<Self>, 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<Bounds<DevicePixels>> {

View file

@ -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();
// impl TextRun {
// fn with_len(&self, len: usize) -> Self {
// let mut this = self.clone();
// this.len = len;
// this
// }
// }
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,
};
// 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];
impl TextRun {
fn with_len(&self, len: usize) -> Self {
let mut this = self.clone();
this.len = len;
this
}
}
// 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::<Vec<_>>(),
// &[
// ShapedBoundary {
// run_ix: 1,
// glyph_ix: 3
// },
// ShapedBoundary {
// run_ix: 2,
// glyph_ix: 3
// },
// ShapedBoundary {
// run_ix: 4,
// glyph_ix: 2
// }
// ],
// );
// });
// }
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();
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
}
],
);
});
}
}

View file

@ -1545,9 +1545,6 @@ impl<'a> WindowContext<'a> {
.finish(&mut self.window.rendered_frame);
ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
self.window.refreshing = false;
self.window.drawing = false;
let previous_focus_path = self.window.rendered_frame.focus_path();
let previous_window_active = self.window.rendered_frame.window_active;
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
@ -1586,6 +1583,8 @@ impl<'a> WindowContext<'a> {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.refreshing = false;
self.window.drawing = false;
}
/// Dispatch a mouse or keyboard event on the window.
@ -2158,7 +2157,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)
}))
}
}

View file

@ -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<LocalTrackPublication>.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<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
return publication.sid as CFString
}

View file

@ -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);

View file

@ -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<Sid> },
RemoteAudioTrackMuteChanged { track_id: Sid, muted: bool },
SubscribedToRemoteVideoTrack(Arc<RemoteVideoTrack>),
SubscribedToRemoteAudioTrack(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
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 },
}

View file

@ -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<ConnectionState>,
watch::Receiver<ConnectionState>,
)>,
remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
update_subscribers: Mutex<Vec<mpsc::UnboundedSender<RoomUpdate>>>,
_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<RemoteAudioTrackUpdate> {
pub fn updates(&self) -> mpsc::UnboundedReceiver<RoomUpdate> {
let (tx, rx) = mpsc::unbounded();
self.remote_audio_track_subscribers.lock().push(tx);
rx
}
pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackUpdate> {
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<String>) {
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<Output = Result<()>> {
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 {
@ -811,7 +864,11 @@ impl RemoteAudioTrack {
impl Drop for RemoteAudioTrack {
fn drop(&mut self) {
unsafe { CFRelease(self.native_track.0) }
// todo: uncomment this `CFRelease`, unless we find that it was causing
// the crash in the `livekit.multicast` thread.
//
// unsafe { CFRelease(self.native_track.0) }
let _ = self.native_track;
}
}
@ -889,18 +946,6 @@ impl Drop for RemoteVideoTrack {
}
}
pub enum RemoteVideoTrackUpdate {
Subscribed(Arc<RemoteVideoTrack>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
pub struct MacOSDisplay(swift::MacOSDisplay);
impl MacOSDisplay {

View file

@ -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<BTreeMap<String, Arc<TestServer>>> = 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<Sid> {
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<Sid> {
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<Vec<Arc<RemoteVideoTrack>>> {
@ -369,39 +380,26 @@ impl live_kit_server::api::Client for TestApiClient {
}
}
pub type Sid = String;
struct RoomState {
connection: (
watch::Sender<ConnectionState>,
watch::Receiver<ConnectionState>,
),
display_sources: Vec<MacOSDisplay>,
audio_track_updates: (
async_broadcast::Sender<RemoteAudioTrackUpdate>,
async_broadcast::Receiver<RemoteAudioTrackUpdate>,
),
video_track_updates: (
async_broadcast::Sender<RemoteVideoTrackUpdate>,
async_broadcast::Receiver<RemoteVideoTrackUpdate>,
),
}
#[derive(Clone, Eq, PartialEq)]
pub enum ConnectionState {
Disconnected,
Connected { url: String, token: String },
updates_tx: async_broadcast::Sender<RoomUpdate>,
updates_rx: async_broadcast::Receiver<RoomUpdate>,
}
pub struct Room(Mutex<RoomState>);
impl Room {
pub fn new() -> Arc<Self> {
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<Item = RemoteAudioTrackUpdate> {
self.0.lock().audio_track_updates.1.clone()
}
pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
self.0.lock().video_track_updates.1.clone()
pub fn updates(&self) -> impl Stream<Item = RoomUpdate> {
self.0.lock().updates_rx.clone()
}
pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {
@ -555,11 +557,27 @@ impl Drop for Room {
}
}
pub struct LocalTrackPublication;
#[derive(Clone)]
pub struct LocalTrackPublication {
sid: String,
muted: Arc<AtomicBool>,
}
impl LocalTrackPublication {
pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {
async { Ok(()) }
pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
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<RemoteVideoTrack>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
#[derive(Clone)]
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
#[derive(Clone)]
pub struct MacOSDisplay {
frames: (

View file

@ -130,7 +130,7 @@ pub struct Project {
next_diagnostic_group_id: usize,
user_store: Model<UserStore>,
fs: Arc<dyn Fs>,
client_state: Option<ProjectClientState>,
client_state: ProjectClientState,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
_subscriptions: Vec<gpui::Subscription>,
@ -254,8 +254,10 @@ enum WorktreeHandle {
Weak(WeakModel<Worktree>),
}
#[derive(Debug)]
enum ProjectClientState {
Local {
Local,
Shared {
remote_id: u64,
updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,
_send_updates: Task<Result<()>>,
@ -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<u64> {
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<Self>) {
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<Self>) -> 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<Self>) -> Task<Result<()>> {
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"
)))

View file

@ -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<DisplayPoint>, 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<DisplayPoint>, Hsla)>| {
background_highlights
.into_iter()

View file

@ -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);

View file

@ -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<Terminal>,
workspace: WeakView<Workspace>,
focus: FocusHandle,
focused: bool,
cursor_visible: bool,
@ -160,6 +162,7 @@ impl StatefulInteractiveElement for TerminalElement {}
impl TerminalElement {
pub fn new(
terminal: Model<Terminal>,
workspace: WeakView<Workspace>,
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<Terminal>,
workspace: WeakView<Workspace>,
cursor_bounds: Option<Bounds<Pixels>>,
}
@ -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();
}

View file

@ -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<Terminal>,
workspace: WeakView<Workspace>,
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>,
) -> 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),

View file

@ -194,9 +194,21 @@ impl settings::Settings for ThemeSettings {
..Default::default()
};
root_schema
.definitions
.extend([("ThemeName".into(), theme_name_schema.into())]);
let available_fonts = cx
.text_system()
.all_font_families()
.into_iter()
.map(Value::String)
.collect();
let fonts_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(available_fonts),
..Default::default()
};
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("FontFamilies".into(), fonts_schema.into()),
]);
root_schema
.schema
@ -204,10 +216,16 @@ impl settings::Settings for ThemeSettings {
.as_mut()
.unwrap()
.properties
.extend([(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
)]);
.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"buffer_font_family".to_owned(),
Schema::new_ref("#/definitions/FontFamilies".into()),
),
]);
root_schema
}

View file

@ -22,7 +22,7 @@ pub struct PlayerColors(pub Vec<PlayerColor>);
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()
}

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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()),

View file

@ -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(

View file

@ -137,6 +137,8 @@ pub trait ResultExt<E> {
type Ok;
fn log_err(self) -> Option<Self::Ok>;
/// Assert that this result should never be an error in development or tests.
fn debug_assert_ok(self, reason: &str) -> Self;
fn warn_on_err(self) -> Option<Self::Ok>;
fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
}
@ -159,6 +161,14 @@ where
}
}
#[track_caller]
fn debug_assert_ok(self, reason: &str) -> Self {
if let Err(error) = &self {
debug_panic!("{reason} - {error:?}");
}
self
}
fn warn_on_err(self) -> Option<T> {
match self {
Ok(value) => Some(value),
@ -234,6 +244,7 @@ where
}
}
#[must_use]
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
impl<F, T, E> Future for LogErrorFuture<F>

View file

@ -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));

View file

@ -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();
}

View file

@ -512,6 +512,11 @@ impl Workspace {
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
let panes_to_unfollow: Vec<View<Pane>> =
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!")
// })
@ -2603,11 +2608,20 @@ impl Workspace {
let cx = &cx;
move |item| {
let item = item.to_followable_item_handle(cx)?;
if (project_id.is_none() || project_id != follower_project_id)
&& item.is_project_item(cx)
// If the item belongs to a particular project, then it should
// only be included if this project is shared, and the follower
// is in thie project.
//
// Some items, like channel notes, do not belong to a particular
// project, so they should be included regardless of whether the
// current project is shared, or what project the follower is in.
if item.is_project_item(cx)
&& (project_id.is_none() || project_id != follower_project_id)
{
return None;
}
let id = item.remote_id(client, cx)?.to_proto();
let variant = item.to_state_proto(cx)?;
Some(proto::View {
@ -2785,8 +2799,12 @@ impl Workspace {
update: proto::update_followers::Variant,
cx: &mut WindowContext,
) -> Option<()> {
// If this update only applies to for followers in the current project,
// then skip it unless this project is shared. If it applies to all
// followers, regardless of project, then set `project_id` to none,
// indicating that it goes to all followers.
let project_id = if project_only {
self.project.read(cx).remote_id()
Some(self.project.read(cx).remote_id()?)
} else {
None
};
@ -4363,12 +4381,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 +4956,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());
#[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 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);
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));
workspace
.right_dock()
.update(cx, |right_dock, cx| right_dock.set_open(true, cx));
// panel
// });
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);
// });
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::<TestPanel>(cx);
// });
// Transfer focus from center to panel
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(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));
// });
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::<TestPanel>(cx);
// });
// Transfer focus from panel to center
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(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));
// });
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);
// });
// 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));
// });
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);
// });
// 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));
// });
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)
// });
// 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));
// });
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::<TestPanel>(cx);
// });
// Transfer focus to the center closes the dock
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(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));
// });
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::<TestPanel>(cx);
// });
// Transferring focus back to the panel keeps it zoomed
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(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));
// });
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)
// });
// 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));
// });
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)
// });
// 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));
// });
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));
// 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());
// });
// }
// 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_panels(cx: &mut gpui::TestAppContext) {
// init_test(cx);
// let fs = FakeFs::new(cx.executor());
struct TestModal(FocusHandle);
// let project = Project::test(fs, [], cx).await;
// let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
impl TestModal {
fn new(cx: &mut ViewContext<Self>) -> Self {
Self(cx.focus_handle())
}
}
// 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));
impl EventEmitter<DismissEvent> for TestModal {}
// 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)
// );
impl FocusableView for TestModal {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.0.clone()
}
}
// 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(),
// );
impl ModalView for TestModal {}
// (panel_1, panel_2)
// });
impl Render for TestModal {
fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
div().track_focus(&self.0)
}
}
// // Move panel_1 to the right
// panel_1.update(cx, |panel_1, cx| {
// panel_1.set_position(DockPosition::Right, cx)
// });
#[gpui::test]
async fn test_panels(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
// 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.);
let project = Project::test(fs, [], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
// // Now we move panel_2 to the left
// panel_2.set_position(DockPosition::Left, 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));
// 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(),
// );
// });
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)
);
// // Move panel_1 back to the left
// panel_1.update(cx, |panel_1, cx| {
// panel_1.set_position(DockPosition::Left, 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(),
);
// 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());
(panel_1, panel_2)
});
// // Now we move panel_1 to the bottom
// panel_1.set_position(DockPosition::Bottom, cx);
// });
// 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, 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);
// });
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.)
);
// // Emit activated event on panel 1
// panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
// Now we move panel_2 to the left
panel_2.set_position(DockPosition::Left, cx);
});
// // 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));
// });
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(),
);
});
// // Emit closed event on panel 2, which is not active
// panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
// Move panel_1 back to the left
panel_1.update(cx, |panel_1, cx| {
panel_1.set_position(DockPosition::Left, cx)
});
// // 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(),
// );
// });
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());
// // 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));
// });
// Now we move panel_1 to the bottom
panel_1.set_position(DockPosition::Bottom, cx);
});
// // 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()));
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);
});
// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
// });
// Emit activated event on panel 1
panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
// // 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));
// });
// 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));
});
// // 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);
// });
// Emit closed event on panel 2, which is not active
panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
// // 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);
// });
// 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(),
);
});
// // 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 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));
});
// // 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);
// });
// 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()));
// // Emit closed event on panel 1, which is active
// panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
});
// // 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());
// });
// }
// 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::<TestModal>(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| {

View file

@ -148,6 +148,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, 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
})

View file

@ -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).

View file

@ -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" ]

View file

@ -1,24 +1,45 @@
#!/usr/bin/env node
const HELP = `
USAGE
zed-local [options] [zed args]
OPTIONS
--help Print this help message
--release Build Zed in release mode
-2, -3, -4 Spawn 2, 3, or 4 Zed instances, with their windows tiled.
--top Arrange the Zed windows so they take up the top half of the screen.
`.trim();
const { spawn, execFileSync } = require("child_process");
const RESOLUTION_REGEX = /(\d+) x (\d+)/;
const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
const RELEASE_MODE = "--release";
let instanceCount = 1;
let isReleaseMode = false;
let isTop = false;
const args = process.argv.slice(2);
while (args.length > 0) {
const arg = args[0];
const digitMatch = arg.match(DIGIT_FLAG_REGEX);
if (digitMatch) {
instanceCount = parseInt(digitMatch[1]);
} else if (arg === "--release") {
isReleaseMode = true;
} else if (arg === "--top") {
isTop = true;
} else if (arg === "--help") {
console.log(HELP);
process.exit(0);
} else {
break;
}
// 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");
}
// Parse the resolution of the main screen
const displayInfo = JSON.parse(
@ -34,7 +55,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;