Merge remote-tracking branch 'origin/main' into cache
# Conflicts: # crates/gpui/src/window.rs
This commit is contained in:
@@ -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")));
|
||||
|
||||
+52
-57
@@ -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 {
|
||||
|
||||
@@ -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))?
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE rooms ADD COLUMN environment TEXT;
|
||||
@@ -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)
|
||||
|
||||
@@ -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))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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. :)
|
||||
|
||||
@@ -23,6 +23,7 @@ theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+320
-315
@@ -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)])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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"
|
||||
)))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+375
-311
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user