Detailed changes
@@ -1380,6 +1380,7 @@ dependencies = [
"schemars",
"serde",
"serde_derive",
+ "serde_json",
"settings",
"smol",
"sum_tree",
@@ -1545,6 +1546,7 @@ dependencies = [
"schemars",
"serde",
"serde_derive",
+ "serde_json",
"settings",
"smallvec",
"theme",
@@ -2490,6 +2492,7 @@ dependencies = [
"regex",
"serde",
"serde_derive",
+ "serde_json",
"settings",
"smallvec",
"smol",
@@ -3784,6 +3787,7 @@ dependencies = [
"lsp",
"project",
"serde",
+ "serde_json",
"settings",
"theme",
"tree-sitter",
@@ -5511,6 +5515,7 @@ dependencies = [
"picker",
"postage",
"project",
+ "serde_json",
"settings",
"smol",
"text",
@@ -7764,6 +7769,7 @@ dependencies = [
"search",
"serde",
"serde_derive",
+ "serde_json",
"settings",
"shellexpand",
"smallvec",
@@ -7844,6 +7850,7 @@ dependencies = [
"pathfinder_color",
"rust-embed",
"serde",
+ "serde_json",
"simplelog",
"strum",
"theme",
@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1,8 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::AsyncReadExt;
+use gpui::AppContext;
use gpui::BackgroundExecutor;
-use gpui::{serde_json, AppContext};
use isahc::http::StatusCode;
use isahc::prelude::Configurable;
use isahc::{AsyncBody, Response};
@@ -11,6 +11,7 @@ use parking_lot::{Mutex, RwLock};
use parse_duration::parse;
use postage::watch;
use serde::{Deserialize, Serialize};
+use serde_json;
use std::env;
use std::ops::Add;
use std::sync::Arc;
@@ -2298,11 +2298,10 @@ impl ConversationEditor {
move |_cx| {
let message_id = message.id;
let sender = ButtonLike::new("role")
+ .style(ButtonStyle::Filled)
.child(match message.role {
Role::User => Label::new("You").color(Color::Default),
- Role::Assistant => {
- Label::new("Assistant").color(Color::Modified)
- }
+ Role::Assistant => Label::new("Assistant").color(Color::Info),
Role::System => Label::new("System").color(Color::Warning),
})
.tooltip(|cx| {
@@ -2325,11 +2324,12 @@ impl ConversationEditor {
}
});
- h_stack()
+ div()
+ .h_flex()
.id(("message_header", message_id.0))
.h_11()
+ .relative()
.gap_1()
- .p_1()
.child(sender)
// TODO: Only show this if the message if the message has been sent
.child(
@@ -2538,7 +2538,7 @@ impl Render for ConversationEditor {
.child(
div()
.size_full()
- .pl_2()
+ .pl_4()
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone()),
)
@@ -3538,5 +3538,5 @@ fn report_assistant_event(
.default_open_ai_model
.clone();
- telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name(), cx)
+ telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name())
}
@@ -310,14 +310,14 @@ impl ActiveCall {
})
}
- pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+ pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
- report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
+ report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -467,7 +467,7 @@ impl ActiveCall {
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
- report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
+ report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
@@ -477,11 +477,10 @@ pub fn report_call_event_for_room(
room_id: u64,
channel_id: Option<u64>,
client: &Arc<Client>,
- cx: &mut AppContext,
) {
let telemetry = client.telemetry();
- telemetry.report_call_event(operation, Some(room_id), channel_id, cx)
+ telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
@@ -494,12 +493,7 @@ pub fn report_call_event_for_channel(
let telemetry = client.telemetry();
- telemetry.report_call_event(
- operation,
- room.map(|r| r.read(cx).id()),
- Some(channel_id),
- cx,
- )
+ telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
@@ -343,12 +343,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
}
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
client::init(&client, cx);
crate::init(&client, user_store, cx);
@@ -36,6 +36,7 @@ rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
+serde_json.workspace = true
smol.workspace = true
sysinfo.workspace = true
tempfile = "3"
@@ -15,8 +15,8 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
- actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
- SemanticVersion, Task, WeakModel,
+ actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
+ WeakModel,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
@@ -25,6 +25,7 @@ use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
+use serde_json;
use settings::Settings;
use std::{
any::TypeId,
@@ -45,7 +46,7 @@ use util::http::HttpClient;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
-pub use telemetry::ClickhouseEvent;
+pub use telemetry::Event;
pub use user::*;
lazy_static! {
@@ -501,8 +502,7 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
- cx.update(|cx| self.telemetry.set_authenticated_user_info(None, false, cx))
- .log_err();
+ self.telemetry.set_authenticated_user_info(None, false);
state._reconnect_task.take();
}
_ => {}
@@ -1405,11 +1405,13 @@ mod tests {
use gpui::{BackgroundExecutor, Context, TestAppContext};
use parking_lot::Mutex;
+ use settings::SettingsStore;
use std::future;
use util::http::FakeHttpClient;
#[gpui::test(iterations = 10)]
async fn test_reconnection(cx: &mut TestAppContext) {
+ init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1444,6 +1446,7 @@ mod tests {
#[gpui::test(iterations = 10)]
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let mut status = client.status();
@@ -1515,6 +1518,7 @@ mod tests {
cx: &mut TestAppContext,
executor: BackgroundExecutor,
) {
+ init_test(cx);
let auth_count = Arc::new(Mutex::new(0));
let dropped_auth_count = Arc::new(Mutex::new(0));
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
@@ -1563,6 +1567,7 @@ mod tests {
#[gpui::test]
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
+ init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1616,6 +1621,7 @@ mod tests {
#[gpui::test]
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
+ init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1644,6 +1650,7 @@ mod tests {
#[gpui::test]
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
+ init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1672,4 +1679,11 @@ mod tests {
id: usize,
subscription: Option<Subscription>,
}
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ });
+ }
}
@@ -1,11 +1,12 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc};
use futures::Future;
-use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
+use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
-use settings::Settings;
+use serde_json;
+use settings::{Settings, SettingsStore};
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
@@ -17,32 +18,32 @@ use util::{channel::ReleaseChannel, TryFutureExt};
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: BackgroundExecutor,
- state: Mutex<TelemetryState>,
+ state: Arc<Mutex<TelemetryState>>,
}
struct TelemetryState {
+ settings: TelemetrySettings,
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
- clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
- flush_clickhouse_events_task: Option<Task<()>>,
+ events_queue: Vec<EventWrapper>,
+ flush_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
}
-const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
+const EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
- static ref CLICKHOUSE_EVENTS_URL: String =
- format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
+ static ref EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, EVENTS_URL_PATH);
}
#[derive(Serialize, Debug)]
-struct ClickhouseEventRequestBody {
+struct EventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
@@ -52,14 +53,14 @@ struct ClickhouseEventRequestBody {
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
- events: Vec<ClickhouseEventWrapper>,
+ events: Vec<EventWrapper>,
}
#[derive(Serialize, Debug)]
-struct ClickhouseEventWrapper {
+struct EventWrapper {
signed_in: bool,
#[serde(flatten)]
- event: ClickhouseEvent,
+ event: Event,
}
#[derive(Serialize, Debug)]
@@ -71,7 +72,7 @@ pub enum AssistantKind {
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
-pub enum ClickhouseEvent {
+pub enum Event {
Editor {
operation: &'static str,
file_extension: Option<String>,
@@ -139,45 +140,61 @@ impl Telemetry {
None
};
+ TelemetrySettings::register(cx);
+
+ let state = Arc::new(Mutex::new(TelemetryState {
+ settings: TelemetrySettings::get_global(cx).clone(),
+ app_metadata: cx.app_metadata(),
+ architecture: env::consts::ARCH,
+ release_channel,
+ installation_id: None,
+ metrics_id: None,
+ session_id: None,
+ events_queue: Default::default(),
+ flush_events_task: Default::default(),
+ log_file: None,
+ is_staff: None,
+ first_event_datetime: None,
+ }));
+
+ cx.observe_global::<SettingsStore>({
+ let state = state.clone();
+
+ move |cx| {
+ let mut state = state.lock();
+ state.settings = TelemetrySettings::get_global(cx).clone();
+ }
+ })
+ .detach();
+
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.background_executor().clone(),
- state: Mutex::new(TelemetryState {
- app_metadata: cx.app_metadata(),
- architecture: env::consts::ARCH,
- release_channel,
- installation_id: None,
- metrics_id: None,
- session_id: None,
- clickhouse_events_queue: Default::default(),
- flush_clickhouse_events_task: Default::default(),
- log_file: None,
- is_staff: None,
- first_event_datetime: None,
- }),
+ state,
});
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
let this = this.clone();
- move |cx| this.shutdown_telemetry(cx)
+ move |_| this.shutdown_telemetry()
}));
this
}
#[cfg(any(test, feature = "test-support"))]
- fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
+ fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
Task::ready(())
}
// Skip calling this function in tests.
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
#[cfg(not(any(test, feature = "test-support")))]
- fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
- self.report_app_event("close", true, cx);
+ fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
+ self.report_app_event("close");
+ self.flush_events();
Task::ready(())
}
@@ -197,7 +214,7 @@ impl Telemetry {
drop(state);
let this = self.clone();
- cx.spawn(|cx| async move {
+ cx.spawn(|_| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory() // For memory usage
@@ -226,11 +243,8 @@ impl Telemetry {
return;
};
- cx.update(|cx| {
- this.report_memory_event(process.memory(), process.virtual_memory(), cx);
- this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32, cx);
- })
- .ok();
+ this.report_memory_event(process.memory(), process.virtual_memory());
+ this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
}
})
.detach();
@@ -240,13 +254,13 @@ impl Telemetry {
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
- cx: &AppContext,
) {
- if !TelemetrySettings::get_global(cx).metrics {
+ let mut state = self.state.lock();
+
+ if !state.settings.metrics {
return;
}
- let mut state = self.state.lock();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
@@ -260,9 +274,8 @@ impl Telemetry {
operation: &'static str,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
- cx: &AppContext,
) {
- let event = ClickhouseEvent::Editor {
+ let event = Event::Editor {
file_extension,
vim_mode,
operation,
@@ -271,7 +284,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
pub fn report_copilot_event(
@@ -279,16 +292,15 @@ impl Telemetry {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
- cx: &AppContext,
) {
- let event = ClickhouseEvent::Copilot {
+ let event = Event::Copilot {
suggestion_id,
suggestion_accepted,
file_extension,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
pub fn report_assistant_event(
@@ -296,16 +308,15 @@ impl Telemetry {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
- cx: &AppContext,
) {
- let event = ClickhouseEvent::Assistant {
+ let event = Event::Assistant {
conversation_id,
kind,
model,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
pub fn report_call_event(
@@ -313,75 +324,58 @@ impl Telemetry {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
- cx: &AppContext,
) {
- let event = ClickhouseEvent::Call {
+ let event = Event::Call {
operation,
room_id,
channel_id,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
- pub fn report_cpu_event(
- self: &Arc<Self>,
- usage_as_percentage: f32,
- core_count: u32,
- cx: &AppContext,
- ) {
- let event = ClickhouseEvent::Cpu {
+ pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
+ let event = Event::Cpu {
usage_as_percentage,
core_count,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
pub fn report_memory_event(
self: &Arc<Self>,
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
- cx: &AppContext,
) {
- let event = ClickhouseEvent::Memory {
+ let event = Event::Memory {
memory_in_bytes,
virtual_memory_in_bytes,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
- pub fn report_app_event(
- self: &Arc<Self>,
- operation: &'static str,
- immediate_flush: bool,
- cx: &AppContext,
- ) {
- let event = ClickhouseEvent::App {
+ pub fn report_app_event(self: &Arc<Self>, operation: &'static str) {
+ let event = Event::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, immediate_flush, cx)
+ self.report_event(event)
}
- pub fn report_setting_event(
- self: &Arc<Self>,
- setting: &'static str,
- value: String,
- cx: &AppContext,
- ) {
- let event = ClickhouseEvent::Setting {
+ pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
+ let event = Event::Setting {
setting,
value,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
- self.report_clickhouse_event(event, false, cx)
+ self.report_event(event)
}
fn milliseconds_since_first_event(&self) -> i64 {
@@ -398,32 +392,26 @@ impl Telemetry {
}
}
- fn report_clickhouse_event(
- self: &Arc<Self>,
- event: ClickhouseEvent,
- immediate_flush: bool,
- cx: &AppContext,
- ) {
- if !TelemetrySettings::get_global(cx).metrics {
+ fn report_event(self: &Arc<Self>, event: Event) {
+ let mut state = self.state.lock();
+
+ if !state.settings.metrics {
return;
}
- let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
- state
- .clickhouse_events_queue
- .push(ClickhouseEventWrapper { signed_in, event });
+ state.events_queue.push(EventWrapper { signed_in, event });
if state.installation_id.is_some() {
- if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
+ if state.events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
- self.flush_clickhouse_events();
+ self.flush_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
- state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
+ state.flush_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
- this.flush_clickhouse_events();
+ this.flush_events();
}));
}
}
@@ -441,11 +429,11 @@ impl Telemetry {
self.state.lock().is_staff
}
- fn flush_clickhouse_events(self: &Arc<Self>) {
+ pub fn flush_events(self: &Arc<Self>) {
let mut state = self.state.lock();
state.first_event_datetime = None;
- let mut events = mem::take(&mut state.clickhouse_events_queue);
- state.flush_clickhouse_events_task.take();
+ let mut events = mem::take(&mut state.events_queue);
+ state.flush_events_task.take();
drop(state);
let this = self.clone();
@@ -466,7 +454,7 @@ impl Telemetry {
{
let state = this.state.lock();
- let request_body = ClickhouseEventRequestBody {
+ let request_body = EventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
@@ -490,7 +478,7 @@ impl Telemetry {
}
this.http_client
- .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
+ .post_json(EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}
@@ -164,7 +164,6 @@ impl UserStore {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
- cx,
)
}
})?;
@@ -888,6 +888,14 @@ impl Database {
.exec(&*tx)
.await?;
+ follower::Entity::delete_many()
+ .filter(
+ Condition::all()
+ .add(follower::Column::FollowerConnectionId.eq(connection.id as i32)),
+ )
+ .exec(&*tx)
+ .await?;
+
// Unshare projects.
project::Entity::delete_many()
.filter(
@@ -1,1890 +1,1777 @@
-//todo!(workspace)
-
-// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-// use call::ActiveCall;
-// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
-// use editor::{Editor, ExcerptRange, MultiBuffer};
-// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
-// use live_kit_client::MacOSDisplay;
-// use project::project_settings::ProjectSettings;
-// use rpc::proto::PeerId;
-// use serde_json::json;
-// use settings::SettingsStore;
-// use std::borrow::Cow;
-// use workspace::{
-// dock::{test::TestPanel, DockPosition},
-// item::{test::TestItem, ItemHandle as _},
-// shared_screen::SharedScreen,
-// SplitDirection, Workspace,
-// };
-
-// #[gpui::test(iterations = 10)]
-// async fn test_basic_following(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// cx_c: &mut TestAppContext,
-// cx_d: &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 client_c = server.create_client(cx_c, "user_c").await;
-// let client_d = server.create_client(cx_d, "user_d").await;
-// server
-// .create_room(&mut [
-// (&client_a, cx_a),
-// (&client_b, cx_b),
-// (&client_c, cx_c),
-// (&client_d, cx_d),
-// ])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "1.txt": "one\none\none",
-// "2.txt": "two\ntwo\ntwo",
-// "3.txt": "three\nthree\nthree",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// let window_a = client_a.build_workspace(&project_a, cx_a);
-// let workspace_a = window_a.root(cx_a).unwrap();
-// let window_b = client_b.build_workspace(&project_b, cx_b);
-// let workspace_b = window_b.root(cx_b).unwrap();
-
-// todo!("could be wrong")
-// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-// let cx_a = &mut cx_a;
-// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-// let cx_b = &mut cx_b;
-// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
-// let cx_c = &mut cx_c;
-// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
-// let cx_d = &mut cx_d;
-
-// // Client A opens some editors.
-// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-// let editor_a1 = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-// let editor_a2 = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Client B opens an editor.
-// let editor_b1 = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// let peer_id_a = client_a.peer_id().unwrap();
-// let peer_id_b = client_b.peer_id().unwrap();
-// let peer_id_c = client_c.peer_id().unwrap();
-// let peer_id_d = client_d.peer_id().unwrap();
-
-// // Client A updates their selections in those editors
-// editor_a1.update(cx_a, |editor, cx| {
-// editor.handle_input("a", cx);
-// editor.handle_input("b", cx);
-// editor.handle_input("c", cx);
-// editor.select_left(&Default::default(), cx);
-// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-// });
-// editor_a2.update(cx_a, |editor, cx| {
-// editor.handle_input("d", cx);
-// editor.handle_input("e", cx);
-// editor.select_left(&Default::default(), cx);
-// assert_eq!(editor.selections.ranges(cx), vec![2..1]);
-// });
-
-// // When client B starts following client A, all visible view states are replicated to client B.
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(peer_id_a, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// cx_c.executor().run_until_parked();
-// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// assert_eq!(
-// cx_b.read(|cx| editor_b2.project_path(cx)),
-// Some((worktree_id, "2.txt").into())
-// );
-// assert_eq!(
-// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-// vec![2..1]
-// );
-// assert_eq!(
-// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-// vec![3..2]
-// );
-
-// cx_c.executor().run_until_parked();
-// let active_call_c = cx_c.read(ActiveCall::global);
-// let project_c = client_c.build_remote_project(project_id, cx_c).await;
-// let window_c = client_c.build_workspace(&project_c, cx_c);
-// let workspace_c = window_c.root(cx_c).unwrap();
-// active_call_c
-// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
-// .await
-// .unwrap();
-// drop(project_c);
-
-// // Client C also follows client A.
-// workspace_c
-// .update(cx_c, |workspace, cx| {
-// workspace.follow(peer_id_a, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// cx_d.executor().run_until_parked();
-// let active_call_d = cx_d.read(ActiveCall::global);
-// let project_d = client_d.build_remote_project(project_id, cx_d).await;
-// let workspace_d = client_d
-// .build_workspace(&project_d, cx_d)
-// .root(cx_d)
-// .unwrap();
-// active_call_d
-// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
-// .await
-// .unwrap();
-// drop(project_d);
-
-// // All clients see that clients B and C are following client A.
-// cx_c.executor().run_until_parked();
-// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-// assert_eq!(
-// followers_by_leader(project_id, cx),
-// &[(peer_id_a, vec![peer_id_b, peer_id_c])],
-// "followers seen by {name}"
-// );
-// }
-
-// // Client C unfollows client A.
-// workspace_c.update(cx_c, |workspace, cx| {
-// workspace.unfollow(&workspace.active_pane().clone(), cx);
-// });
-
-// // All clients see that clients B is following client A.
-// cx_c.executor().run_until_parked();
-// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-// assert_eq!(
-// followers_by_leader(project_id, cx),
-// &[(peer_id_a, vec![peer_id_b])],
-// "followers seen by {name}"
-// );
-// }
-
-// // Client C re-follows client A.
-// workspace_c
-// .update(cx_c, |workspace, cx| {
-// workspace.follow(peer_id_a, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// // All clients see that clients B and C are following client A.
-// cx_c.executor().run_until_parked();
-// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-// assert_eq!(
-// followers_by_leader(project_id, cx),
-// &[(peer_id_a, vec![peer_id_b, peer_id_c])],
-// "followers seen by {name}"
-// );
-// }
-
-// // Client D follows client B, then switches to following client C.
-// workspace_d
-// .update(cx_d, |workspace, cx| {
-// workspace.follow(peer_id_b, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// workspace_d
-// .update(cx_d, |workspace, cx| {
-// workspace.follow(peer_id_c, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// // All clients see that D is following C
-// cx_d.executor().run_until_parked();
-// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-// assert_eq!(
-// followers_by_leader(project_id, cx),
-// &[
-// (peer_id_a, vec![peer_id_b, peer_id_c]),
-// (peer_id_c, vec![peer_id_d])
-// ],
-// "followers seen by {name}"
-// );
-// }
-
-// // Client C closes the project.
-// window_c.remove(cx_c);
-// cx_c.drop_last(workspace_c);
-
-// // Clients A and B see that client B is following A, and client C is not present in the followers.
-// cx_c.executor().run_until_parked();
-// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-// assert_eq!(
-// followers_by_leader(project_id, cx),
-// &[(peer_id_a, vec![peer_id_b]),],
-// "followers seen by {name}"
-// );
-// }
-
-// // When client A activates a different editor, client B does so as well.
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.activate_item(&editor_a1, cx)
-// });
-// executor.run_until_parked();
-// workspace_b.update(cx_b, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// editor_b1.item_id()
-// );
-// });
-
-// // When client A opens a multibuffer, client B does so as well.
-// let multibuffer_a = cx_a.build_model(|cx| {
-// let buffer_a1 = project_a.update(cx, |project, cx| {
-// project
-// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
-// .unwrap()
-// });
-// let buffer_a2 = project_a.update(cx, |project, cx| {
-// project
-// .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
-// .unwrap()
-// });
-// let mut result = MultiBuffer::new(0);
-// result.push_excerpts(
-// buffer_a1,
-// [ExcerptRange {
-// context: 0..3,
-// primary: None,
-// }],
-// cx,
-// );
-// result.push_excerpts(
-// buffer_a2,
-// [ExcerptRange {
-// context: 4..7,
-// primary: None,
-// }],
-// cx,
-// );
-// result
-// });
-// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
-// let editor =
-// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
-// workspace.add_item(Box::new(editor.clone()), cx);
-// editor
-// });
-// executor.run_until_parked();
-// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// assert_eq!(
-// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
-// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
-// );
-
-// // When client A navigates back and forth, client B does so as well.
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// workspace_b.update(cx_b, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// editor_b1.item_id()
-// );
-// });
-
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// workspace_b.update(cx_b, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// editor_b2.item_id()
-// );
-// });
-
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.go_forward(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// workspace_b.update(cx_b, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// editor_b1.item_id()
-// );
-// });
-
-// // Changes to client A's editor are reflected on client B.
-// editor_a1.update(cx_a, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
-// });
-// executor.run_until_parked();
-// editor_b1.update(cx_b, |editor, cx| {
-// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
-// });
-
-// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-// executor.run_until_parked();
-// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
-
-// editor_a1.update(cx_a, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-// editor.set_scroll_position(point(0., 100.), cx);
-// });
-// executor.run_until_parked();
-// editor_b1.update(cx_b, |editor, cx| {
-// assert_eq!(editor.selections.ranges(cx), &[3..3]);
-// });
-
-// // After unfollowing, client B stops receiving updates from client A.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.unfollow(&workspace.active_pane().clone(), cx)
-// });
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.activate_item(&editor_a2, cx)
-// });
-// executor.run_until_parked();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, cx| workspace
-// .active_item(cx)
-// .unwrap()
-// .item_id()),
-// editor_b1.item_id()
-// );
-
-// // Client A starts following client B.
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.follow(peer_id_b, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-// Some(peer_id_b)
-// );
-// assert_eq!(
-// workspace_a.update(cx_a, |workspace, cx| workspace
-// .active_item(cx)
-// .unwrap()
-// .item_id()),
-// editor_a1.item_id()
-// );
-
-// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
-// let display = MacOSDisplay::new();
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(None, cx))
-// .await
-// .unwrap();
-// active_call_b
-// .update(cx_b, |call, cx| {
-// call.room().unwrap().update(cx, |room, cx| {
-// room.set_display_sources(vec![display.clone()]);
-// room.share_screen(cx)
-// })
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .expect("no active item")
-// .downcast::<SharedScreen>()
-// .expect("active item isn't a shared screen")
-// });
-
-// // Client B activates Zed again, which causes the previous editor to become focused again.
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// workspace_a.update(cx_a, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// editor_a1.item_id()
-// )
-// });
-
-// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.activate_item(&multibuffer_editor_b, cx)
-// });
-// executor.run_until_parked();
-// workspace_a.update(cx_a, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// multibuffer_editor_a.item_id()
-// )
-// });
-
-// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.add_panel(panel, cx);
-// workspace.toggle_panel_focus::<TestPanel>(cx);
-// });
-// executor.run_until_parked();
-// assert_eq!(
-// workspace_a.update(cx_a, |workspace, cx| workspace
-// .active_item(cx)
-// .unwrap()
-// .item_id()),
-// shared_screen.item_id()
-// );
-
-// // Toggling the focus back to the pane causes client A to return to the multibuffer.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.toggle_panel_focus::<TestPanel>(cx);
-// });
-// executor.run_until_parked();
-// workspace_a.update(cx_a, |workspace, cx| {
-// assert_eq!(
-// workspace.active_item(cx).unwrap().item_id(),
-// multibuffer_editor_a.item_id()
-// )
-// });
-
-// // Client B activates an item that doesn't implement following,
-// // so the previously-opened screen-sharing item gets activated.
-// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
-// })
-// });
-// executor.run_until_parked();
-// assert_eq!(
-// workspace_a.update(cx_a, |workspace, cx| workspace
-// .active_item(cx)
-// .unwrap()
-// .item_id()),
-// shared_screen.item_id()
-// );
-
-// // Following interrupts when client B disconnects.
-// client_b.disconnect(&cx_b.to_async());
-// executor.advance_clock(RECONNECT_TIMEOUT);
-// assert_eq!(
-// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-// None
-// );
-// }
-
-// #[gpui::test]
-// async fn test_following_tab_order(
-// 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;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "1.txt": "one",
-// "2.txt": "two",
-// "3.txt": "three",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// let workspace_a = client_a
-// .build_workspace(&project_a, cx_a)
-// .root(cx_a)
-// .unwrap();
-// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-
-// let workspace_b = client_b
-// .build_workspace(&project_b, cx_b)
-// .root(cx_b)
-// .unwrap();
-// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-
-// let client_b_id = project_a.update(cx_a, |project, _| {
-// project.collaborators().values().next().unwrap().peer_id
-// });
-
-// //Open 1, 3 in that order on client A
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
-// pane.update(cx, |pane, cx| {
-// pane.items()
-// .map(|item| {
-// item.project_path(cx)
-// .unwrap()
-// .path
-// .to_str()
-// .unwrap()
-// .to_owned()
-// })
-// .collect::<Vec<_>>()
-// })
-// };
-
-// //Verify that the tabs opened in the order we expect
-// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
-
-// //Follow client B as client A
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.follow(client_b_id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// //Open just 2 on client B
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-
-// // Verify that newly opened followed file is at the end
-// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-
-// //Open just 1 on client B
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
-// executor.run_until_parked();
-
-// // Verify that following into 1 did not reorder
-// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_peers_following_each_other(
-// 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;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// // Client A shares a project.
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "1.txt": "one",
-// "2.txt": "two",
-// "3.txt": "three",
-// "4.txt": "four",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// // Client B joins the project.
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// // Client A opens a file.
-// let workspace_a = client_a
-// .build_workspace(&project_a, cx_a)
-// .root(cx_a)
-// .unwrap();
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Client B opens a different file.
-// let workspace_b = client_b
-// .build_workspace(&project_b, cx_b)
-// .root(cx_b)
-// .unwrap();
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Clients A and B follow each other in split panes
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-// });
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
-// })
-// .await
-// .unwrap();
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-// });
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// // Clients A and B return focus to the original files they had open
-// workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
-// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-// executor.run_until_parked();
-
-// // Both clients see the other client's focused file in their right pane.
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(true, "1.txt".into())]
-// },
-// PaneSummary {
-// active: false,
-// leader: client_b.peer_id(),
-// items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(true, "2.txt".into())]
-// },
-// PaneSummary {
-// active: false,
-// leader: client_a.peer_id(),
-// items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
-// },
-// ]
-// );
-
-// // Clients A and B each open a new file.
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "4.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-
-// // Both client's see the other client open the new file, but keep their
-// // focus on their own active pane.
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: false,
-// leader: client_b.peer_id(),
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (true, "4.txt".into())
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: false,
-// leader: client_a.peer_id(),
-// items: vec![
-// (false, "2.txt".into()),
-// (false, "1.txt".into()),
-// (true, "3.txt".into())
-// ]
-// },
-// ]
-// );
-
-// // Client A focuses their right pane, in which they're following client B.
-// workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
-// executor.run_until_parked();
-
-// // Client B sees that client A is now looking at the same file as them.
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_b.peer_id(),
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (true, "4.txt".into())
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: false,
-// leader: client_a.peer_id(),
-// items: vec![
-// (false, "2.txt".into()),
-// (false, "1.txt".into()),
-// (false, "3.txt".into()),
-// (true, "4.txt".into())
-// ]
-// },
-// ]
-// );
-
-// // Client B focuses their right pane, in which they're following client A,
-// // who is following them.
-// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-// executor.run_until_parked();
-
-// // Client A sees that client B is now looking at the same file as them.
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_a.peer_id(),
-// items: vec![
-// (false, "2.txt".into()),
-// (false, "1.txt".into()),
-// (false, "3.txt".into()),
-// (true, "4.txt".into())
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_b.peer_id(),
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (true, "4.txt".into())
-// ]
-// },
-// ]
-// );
-
-// // Client B focuses a file that they previously followed A to, breaking
-// // the follow.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.activate_prev_item(true, cx);
-// });
-// });
-// executor.run_until_parked();
-
-// // Both clients see that client B is looking at that previous file.
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![
-// (false, "2.txt".into()),
-// (false, "1.txt".into()),
-// (true, "3.txt".into()),
-// (false, "4.txt".into())
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_b.peer_id(),
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (false, "4.txt".into()),
-// (true, "3.txt".into()),
-// ]
-// },
-// ]
-// );
-
-// // Client B closes tabs, some of which were originally opened by client A,
-// // and some of which were originally opened by client B.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.close_inactive_items(&Default::default(), cx)
-// .unwrap()
-// .detach();
-// });
-// });
-
-// executor.run_until_parked();
-
-// // Both clients see that Client B is looking at the previous tab.
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![(true, "3.txt".into()),]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_b.peer_id(),
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (false, "4.txt".into()),
-// (true, "3.txt".into()),
-// ]
-// },
-// ]
-// );
-
-// // Client B follows client A again.
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// // Client A cycles through some tabs.
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.activate_prev_item(true, cx);
-// });
-// });
-// executor.run_until_parked();
-
-// // Client B follows client A into those tabs.
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![
-// (false, "1.txt".into()),
-// (false, "2.txt".into()),
-// (true, "4.txt".into()),
-// (false, "3.txt".into()),
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_a.peer_id(),
-// items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
-// },
-// ]
-// );
-
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.activate_prev_item(true, cx);
-// });
-// });
-// executor.run_until_parked();
-
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![
-// (false, "1.txt".into()),
-// (true, "2.txt".into()),
-// (false, "4.txt".into()),
-// (false, "3.txt".into()),
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_a.peer_id(),
-// items: vec![
-// (false, "3.txt".into()),
-// (false, "4.txt".into()),
-// (true, "2.txt".into())
-// ]
-// },
-// ]
-// );
-
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace.active_pane().update(cx, |pane, cx| {
-// pane.activate_prev_item(true, cx);
-// });
-// });
-// executor.run_until_parked();
-
-// assert_eq!(
-// pane_summaries(&workspace_a, cx_a),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: None,
-// items: vec![
-// (true, "1.txt".into()),
-// (false, "2.txt".into()),
-// (false, "4.txt".into()),
-// (false, "3.txt".into()),
-// ]
-// },
-// ]
-// );
-// assert_eq!(
-// pane_summaries(&workspace_b, cx_b),
-// &[
-// PaneSummary {
-// active: false,
-// leader: None,
-// items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-// },
-// PaneSummary {
-// active: true,
-// leader: client_a.peer_id(),
-// items: vec![
-// (false, "3.txt".into()),
-// (false, "4.txt".into()),
-// (false, "2.txt".into()),
-// (true, "1.txt".into()),
-// ]
-// },
-// ]
-// );
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_auto_unfollowing(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// // 2 clients connect to a server.
-// 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;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// // Client A shares a project.
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "1.txt": "one",
-// "2.txt": "two",
-// "3.txt": "three",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// todo!("could be wrong")
-// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-// let cx_a = &mut cx_a;
-// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-// let cx_b = &mut cx_b;
-
-// // Client A opens some editors.
-// let workspace_a = client_a
-// .build_workspace(&project_a, cx_a)
-// .root(cx_a)
-// .unwrap();
-// let _editor_a1 = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Client B starts following client A.
-// let workspace_b = client_b
-// .build_workspace(&project_b, cx_b)
-// .root(cx_b)
-// .unwrap();
-// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-// let leader_id = project_b.update(cx_b, |project, _| {
-// project.collaborators().values().next().unwrap().peer_id
-// });
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(leader_id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-
-// // When client B moves, it automatically stops following client A.
-// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// None
-// );
-
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(leader_id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-
-// // When client B edits, it automatically stops following client A.
-// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// None
-// );
-
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(leader_id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-
-// // When client B scrolls, it automatically stops following client A.
-// editor_b2.update(cx_b, |editor, cx| {
-// editor.set_scroll_position(point(0., 3.), cx)
-// });
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// None
-// );
-
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(leader_id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-
-// // When client B activates a different pane, it continues following client A in the original pane.
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
-// });
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-
-// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// Some(leader_id)
-// );
-
-// // When client B activates a different item in the original pane, it automatically stops following client A.
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-// None
-// );
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_peers_simultaneously_following_each_other(
-// 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;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// client_a.fs().insert_tree("/a", json!({})).await;
-// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-// let workspace_a = client_a
-// .build_workspace(&project_a, cx_a)
-// .root(cx_a)
-// .unwrap();
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// let workspace_b = client_b
-// .build_workspace(&project_b, cx_b)
-// .root(cx_b)
-// .unwrap();
-
-// executor.run_until_parked();
-// let client_a_id = project_b.update(cx_b, |project, _| {
-// project.collaborators().values().next().unwrap().peer_id
-// });
-// let client_b_id = project_a.update(cx_a, |project, _| {
-// project.collaborators().values().next().unwrap().peer_id
-// });
-
-// let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
-// workspace.follow(client_b_id, cx).unwrap()
-// });
-// let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
-// workspace.follow(client_a_id, cx).unwrap()
-// });
-
-// futures::try_join!(a_follow_b, b_follow_a).unwrap();
-// workspace_a.update(cx_a, |workspace, _| {
-// assert_eq!(
-// workspace.leader_for_pane(workspace.active_pane()),
-// Some(client_b_id)
-// );
-// });
-// workspace_b.update(cx_b, |workspace, _| {
-// assert_eq!(
-// workspace.leader_for_pane(workspace.active_pane()),
-// Some(client_a_id)
-// );
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_following_across_workspaces(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// // a and b join a channel/call
-// // a shares project 1
-// // b shares project 2
-// //
-// // b follows a: causes project 2 to be joined, and b to follow a.
-// // b opens a different file in project 2, a follows b
-// // b opens a different file in project 1, a cannot follow b
-// // b shares the project, a joins the project and follows b
-// 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;
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "w.rs": "",
-// "x.rs": "",
-// }),
-// )
-// .await;
-
-// client_b
-// .fs()
-// .insert_tree(
-// "/b",
-// json!({
-// "y.rs": "",
-// "z.rs": "",
-// }),
-// )
-// .await;
-
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
-// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
-
-// let workspace_a = client_a
-// .build_workspace(&project_a, cx_a)
-// .root(cx_a)
-// .unwrap();
-// let workspace_b = client_b
-// .build_workspace(&project_b, cx_b)
-// .root(cx_b)
-// .unwrap();
-
-// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
-// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
-
-// active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// todo!("could be wrong")
-// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-// let cx_a = &mut cx_a;
-// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-// let cx_b = &mut cx_b;
-
-// workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// executor.run_until_parked();
-// assert_eq!(visible_push_notifications(cx_b).len(), 1);
-
-// workspace_b.update(cx_b, |workspace, cx| {
-// workspace
-// .follow(client_a.peer_id().unwrap(), cx)
-// .unwrap()
-// .detach()
-// });
-
-// executor.run_until_parked();
-// let workspace_b_project_a = cx_b
-// .windows()
-// .iter()
-// .max_by_key(|window| window.item_id())
-// .unwrap()
-// .downcast::<Workspace>()
-// .unwrap()
-// .root(cx_b)
-// .unwrap();
-
-// // assert that b is following a in project a in w.rs
-// workspace_b_project_a.update(cx_b, |workspace, cx| {
-// assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
-// assert_eq!(
-// client_a.peer_id(),
-// workspace.leader_for_pane(workspace.active_pane())
-// );
-// let item = workspace.active_item(cx).unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
-// });
-
-// // TODO: in app code, this would be done by the collab_ui.
-// active_call_b
-// .update(cx_b, |call, cx| {
-// let project = workspace_b_project_a.read(cx).project().clone();
-// call.set_location(Some(&project), cx)
-// })
-// .await
-// .unwrap();
-
-// // assert that there are no share notifications open
-// assert_eq!(visible_push_notifications(cx_b).len(), 0);
-
-// // b moves to x.rs in a's project, and a follows
-// workspace_b_project_a
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// executor.run_until_parked();
-// workspace_b_project_a.update(cx_b, |workspace, cx| {
-// let item = workspace.active_item(cx).unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
-// });
-
-// workspace_a.update(cx_a, |workspace, cx| {
-// workspace
-// .follow(client_b.peer_id().unwrap(), cx)
-// .unwrap()
-// .detach()
-// });
-
-// executor.run_until_parked();
-// workspace_a.update(cx_a, |workspace, cx| {
-// assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
-// assert_eq!(
-// client_b.peer_id(),
-// workspace.leader_for_pane(workspace.active_pane())
-// );
-// let item = workspace.active_pane().read(cx).active_item().unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
-// });
-
-// // b moves to y.rs in b's project, a is still following but can't yet see
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// // TODO: in app code, this would be done by the collab_ui.
-// active_call_b
-// .update(cx_b, |call, cx| {
-// let project = workspace_b.read(cx).project().clone();
-// call.set_location(Some(&project), cx)
-// })
-// .await
-// .unwrap();
-
-// let project_b_id = active_call_b
-// .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
-// .await
-// .unwrap();
-
-// executor.run_until_parked();
-// assert_eq!(visible_push_notifications(cx_a).len(), 1);
-// cx_a.update(|cx| {
-// workspace::join_remote_project(
-// project_b_id,
-// client_b.user_id().unwrap(),
-// client_a.app_state.clone(),
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-
-// executor.run_until_parked();
-
-// assert_eq!(visible_push_notifications(cx_a).len(), 0);
-// let workspace_a_project_b = cx_a
-// .windows()
-// .iter()
-// .max_by_key(|window| window.item_id())
-// .unwrap()
-// .downcast::<Workspace>()
-// .unwrap()
-// .root(cx_a)
-// .unwrap();
-
-// workspace_a_project_b.update(cx_a, |workspace, cx| {
-// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
-// assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
-// assert_eq!(
-// client_b.peer_id(),
-// workspace.leader_for_pane(workspace.active_pane())
-// );
-// let item = workspace.active_item(cx).unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
-// });
-// }
-
-// #[gpui::test]
-// async fn test_following_into_excluded_file(
-// executor: BackgroundExecutor,
-// mut cx_a: &mut TestAppContext,
-// mut 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;
-// for cx in [&mut cx_a, &mut cx_b] {
-// cx.update(|cx| {
-// cx.update_global::<SettingsStore, _>(|store, cx| {
-// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
-// });
-// });
-// });
-// }
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// ".git": {
-// "COMMIT_EDITMSG": "write your commit message here",
-// },
-// "1.txt": "one\none\none",
-// "2.txt": "two\ntwo\ntwo",
-// "3.txt": "three\nthree\nthree",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// let window_a = client_a.build_workspace(&project_a, cx_a);
-// let workspace_a = window_a.root(cx_a).unwrap();
-// let peer_id_a = client_a.peer_id().unwrap();
-// let window_b = client_b.build_workspace(&project_b, cx_b);
-// let workspace_b = window_b.root(cx_b).unwrap();
-
-// todo!("could be wrong")
-// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-// let cx_a = &mut cx_a;
-// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-// let cx_b = &mut cx_b;
-
-// // Client A opens editors for a regular file and an excluded file.
-// let editor_for_regular = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-// let editor_for_excluded_a = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Client A updates their selections in those editors
-// editor_for_regular.update(cx_a, |editor, cx| {
-// editor.handle_input("a", cx);
-// editor.handle_input("b", cx);
-// editor.handle_input("c", cx);
-// editor.select_left(&Default::default(), cx);
-// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-// });
-// editor_for_excluded_a.update(cx_a, |editor, cx| {
-// editor.select_all(&Default::default(), cx);
-// editor.handle_input("new commit message", cx);
-// editor.select_left(&Default::default(), cx);
-// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
-// });
-
-// // When client B starts following client A, currently visible file is replicated
-// workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.follow(peer_id_a, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// assert_eq!(
-// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
-// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
-// );
-// assert_eq!(
-// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-// vec![18..17]
-// );
-
-// // Changes from B to the excluded file are replicated in A's editor
-// editor_for_excluded_b.update(cx_b, |editor, cx| {
-// editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
-// });
-// executor.run_until_parked();
-// editor_for_excluded_a.update(cx_a, |editor, cx| {
-// assert_eq!(
-// editor.text(cx),
-// "new commit messag\nCo-Authored-By: B <b@b.b>"
-// );
-// });
-// }
-
-// fn visible_push_notifications(
-// cx: &mut TestAppContext,
-// ) -> Vec<gpui::View<ProjectSharedNotification>> {
-// let mut ret = Vec::new();
-// for window in cx.windows() {
-// window.update(cx, |window| {
-// if let Some(handle) = window
-// .root_view()
-// .clone()
-// .downcast::<ProjectSharedNotification>()
-// {
-// ret.push(handle)
-// }
-// });
-// }
-// ret
-// }
-
-// #[derive(Debug, PartialEq, Eq)]
-// struct PaneSummary {
-// active: bool,
-// leader: Option<PeerId>,
-// items: Vec<(bool, String)>,
-// }
-
-// fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
-// cx.read(|cx| {
-// let active_call = ActiveCall::global(cx).read(cx);
-// let peer_id = active_call.client().peer_id();
-// let room = active_call.room().unwrap().read(cx);
-// let mut result = room
-// .remote_participants()
-// .values()
-// .map(|participant| participant.peer_id)
-// .chain(peer_id)
-// .filter_map(|peer_id| {
-// let followers = room.followers_for(peer_id, project_id);
-// if followers.is_empty() {
-// None
-// } else {
-// Some((peer_id, followers.to_vec()))
-// }
-// })
-// .collect::<Vec<_>>();
-// result.sort_by_key(|e| e.0);
-// result
-// })
-// }
-
-// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
-// workspace.update(cx, |workspace, cx| {
-// let active_pane = workspace.active_pane();
-// workspace
-// .panes()
-// .iter()
-// .map(|pane| {
-// let leader = workspace.leader_for_pane(pane);
-// let active = pane == active_pane;
-// let pane = pane.read(cx);
-// let active_ix = pane.active_item_index();
-// PaneSummary {
-// active,
-// leader,
-// items: pane
-// .items()
-// .enumerate()
-// .map(|(ix, item)| {
-// (
-// ix == active_ix,
-// item.tab_description(0, cx)
-// .map_or(String::new(), |s| s.to_string()),
-// )
-// })
-// .collect(),
-// }
-// })
-// .collect()
-// })
-// }
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
+use editor::{Editor, ExcerptRange, MultiBuffer};
+use gpui::{
+ point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext,
+ VisualTestContext,
+};
+use language::Capability;
+use live_kit_client::MacOSDisplay;
+use project::project_settings::ProjectSettings;
+use rpc::proto::PeerId;
+use serde_json::json;
+use settings::SettingsStore;
+use workspace::{
+ dock::{test::TestPanel, DockPosition},
+ item::{test::TestItem, ItemHandle as _},
+ shared_screen::SharedScreen,
+ SplitDirection, Workspace,
+};
+
+#[gpui::test(iterations = 10)]
+async fn test_basic_following(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+ cx_d: &mut TestAppContext,
+) {
+ let executor = cx_a.executor();
+ 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 client_c = server.create_client(cx_c, "user_c").await;
+ let client_d = server.create_client(cx_d, "user_d").await;
+ server
+ .create_room(&mut [
+ (&client_a, cx_a),
+ (&client_b, cx_b),
+ (&client_c, cx_c),
+ (&client_d, cx_d),
+ ])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "1.txt": "one\none\none",
+ "2.txt": "two\ntwo\ntwo",
+ "3.txt": "three\nthree\nthree",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ 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);
+
+ // Client A opens some editors.
+ let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
+ let editor_a1 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let editor_a2 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B opens an editor.
+ let editor_b1 = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let peer_id_a = client_a.peer_id().unwrap();
+ let peer_id_b = client_b.peer_id().unwrap();
+ let peer_id_c = client_c.peer_id().unwrap();
+ let peer_id_d = client_d.peer_id().unwrap();
+
+ // Client A updates their selections in those editors
+ editor_a1.update(cx_a, |editor, cx| {
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+ });
+ editor_a2.update(cx_a, |editor, cx| {
+ editor.handle_input("d", cx);
+ editor.handle_input("e", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![2..1]);
+ });
+
+ // When client B starts following client A, all visible view states are replicated to client B.
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+ cx_c.executor().run_until_parked();
+ let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_b2.project_path(cx)),
+ Some((worktree_id, "2.txt").into())
+ );
+ assert_eq!(
+ editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![2..1]
+ );
+ assert_eq!(
+ editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![3..2]
+ );
+
+ executor.run_until_parked();
+ let active_call_c = cx_c.read(ActiveCall::global);
+ let project_c = client_c.build_remote_project(project_id, cx_c).await;
+ let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
+ active_call_c
+ .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+ .await
+ .unwrap();
+ let weak_project_c = project_c.downgrade();
+ drop(project_c);
+
+ // Client C also follows client A.
+ workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+ cx_d.executor().run_until_parked();
+ let active_call_d = cx_d.read(ActiveCall::global);
+ let project_d = client_d.build_remote_project(project_id, cx_d).await;
+ let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
+ active_call_d
+ .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
+ .await
+ .unwrap();
+ drop(project_d);
+
+ // All clients see that clients B and C are following client A.
+ cx_c.executor().run_until_parked();
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
+ }
+
+ // Client C unfollows client A.
+ workspace_c.update(cx_c, |workspace, cx| {
+ workspace.unfollow(&workspace.active_pane().clone(), cx);
+ });
+
+ // All clients see that clients B is following client A.
+ cx_c.executor().run_until_parked();
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b])],
+ "followers seen by {name}"
+ );
+ }
+
+ // Client C re-follows client A.
+ workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+ // All clients see that clients B and C are following client A.
+ cx_c.executor().run_until_parked();
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
+ }
+
+ // Client D follows client B, then switches to following client C.
+ workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx));
+ cx_a.executor().run_until_parked();
+ workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx));
+
+ // All clients see that D is following C
+ cx_a.executor().run_until_parked();
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[
+ (peer_id_a, vec![peer_id_b, peer_id_c]),
+ (peer_id_c, vec![peer_id_d])
+ ],
+ "followers seen by {name}"
+ );
+ }
+
+ // Client C closes the project.
+ let weak_workspace_c = workspace_c.downgrade();
+ workspace_c.update(cx_c, |workspace, cx| {
+ workspace.close_window(&Default::default(), cx);
+ });
+ cx_c.update(|_| {
+ drop(workspace_c);
+ });
+ cx_b.executor().run_until_parked();
+ // are you sure you want to leave the call?
+ cx_c.simulate_prompt_answer(0);
+ cx_b.executor().run_until_parked();
+ executor.run_until_parked();
+
+ weak_workspace_c.assert_dropped();
+ weak_project_c.assert_dropped();
+
+ // Clients A and B see that client B is following A, and client C is not present in the followers.
+ executor.run_until_parked();
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b]),],
+ "followers seen by {name}"
+ );
+ }
+
+ // When client A activates a different editor, client B does so as well.
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.activate_item(&editor_a1, cx)
+ });
+ executor.run_until_parked();
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ editor_b1.item_id()
+ );
+ });
+
+ // When client A opens a multibuffer, client B does so as well.
+ let multibuffer_a = cx_a.new_model(|cx| {
+ let buffer_a1 = project_a.update(cx, |project, cx| {
+ project
+ .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+ .unwrap()
+ });
+ let buffer_a2 = project_a.update(cx, |project, cx| {
+ project
+ .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+ .unwrap()
+ });
+ let mut result = MultiBuffer::new(0, Capability::ReadWrite);
+ result.push_excerpts(
+ buffer_a1,
+ [ExcerptRange {
+ context: 0..3,
+ primary: None,
+ }],
+ cx,
+ );
+ result.push_excerpts(
+ buffer_a2,
+ [ExcerptRange {
+ context: 4..7,
+ primary: None,
+ }],
+ cx,
+ );
+ result
+ });
+ let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
+ let editor =
+ cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+ workspace.add_item(Box::new(editor.clone()), cx);
+ editor
+ });
+ executor.run_until_parked();
+ let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
+ multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
+ );
+
+ // When client A navigates back and forth, client B does so as well.
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ editor_b1.item_id()
+ );
+ });
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ editor_b2.item_id()
+ );
+ });
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.go_forward(workspace.active_pane().downgrade(), cx)
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ editor_b1.item_id()
+ );
+ });
+
+ // Changes to client A's editor are reflected on client B.
+ editor_a1.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
+ });
+ executor.run_until_parked();
+ cx_b.background_executor.run_until_parked();
+ editor_b1.update(cx_b, |editor, cx| {
+ assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+ });
+
+ editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+ executor.run_until_parked();
+ editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+
+ editor_a1.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+ editor.set_scroll_position(point(0., 100.), cx);
+ });
+ executor.run_until_parked();
+ editor_b1.update(cx_b, |editor, cx| {
+ assert_eq!(editor.selections.ranges(cx), &[3..3]);
+ });
+
+ // After unfollowing, client B stops receiving updates from client A.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.unfollow(&workspace.active_pane().clone(), cx)
+ });
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.activate_item(&editor_a2, cx)
+ });
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .item_id()),
+ editor_b1.item_id()
+ );
+
+ // Client A starts following client B.
+ workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx));
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ Some(peer_id_b)
+ );
+ assert_eq!(
+ workspace_a.update(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .item_id()),
+ editor_a1.item_id()
+ );
+
+ // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+ let display = MacOSDisplay::new();
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(None, cx))
+ .await
+ .unwrap();
+ active_call_b
+ .update(cx_b, |call, cx| {
+ call.room().unwrap().update(cx, |room, cx| {
+ room.set_display_sources(vec![display.clone()]);
+ room.share_screen(cx)
+ })
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .expect("no active item")
+ .downcast::<SharedScreen>()
+ .expect("active item isn't a shared screen")
+ });
+
+ // Client B activates Zed again, which causes the previous editor to become focused again.
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ editor_a1.item_id()
+ )
+ });
+
+ // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.activate_item(&multibuffer_editor_b, cx)
+ });
+ executor.run_until_parked();
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ multibuffer_editor_a.item_id()
+ )
+ });
+
+ // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
+ let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.add_panel(panel, cx);
+ workspace.toggle_panel_focus::<TestPanel>(cx);
+ });
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_a.update(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .item_id()),
+ shared_screen.item_id()
+ );
+
+ // Toggling the focus back to the pane causes client A to return to the multibuffer.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.toggle_panel_focus::<TestPanel>(cx);
+ });
+ executor.run_until_parked();
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().item_id(),
+ multibuffer_editor_a.item_id()
+ )
+ });
+
+ // Client B activates an item that doesn't implement following,
+ // so the previously-opened screen-sharing item gets activated.
+ let unfollowable_item = cx_b.new_view(|cx| TestItem::new(cx));
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
+ })
+ });
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_a.update(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .item_id()),
+ shared_screen.item_id()
+ );
+
+ // Following interrupts when client B disconnects.
+ client_b.disconnect(&cx_b.to_async());
+ executor.advance_clock(RECONNECT_TIMEOUT);
+ assert_eq!(
+ workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ None
+ );
+}
+
+#[gpui::test]
+async fn test_following_tab_order(
+ 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;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
+
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+
+ let client_b_id = project_a.update(cx_a, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+
+ //Open 1, 3 in that order on client A
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
+ pane.update(cx, |pane, cx| {
+ pane.items()
+ .map(|item| {
+ item.project_path(cx)
+ .unwrap()
+ .path
+ .to_str()
+ .unwrap()
+ .to_owned()
+ })
+ .collect::<Vec<_>>()
+ })
+ };
+
+ //Verify that the tabs opened in the order we expect
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
+
+ //Follow client B as client A
+ workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
+ executor.run_until_parked();
+
+ //Open just 2 on client B
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+
+ // Verify that newly opened followed file is at the end
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+
+ //Open just 1 on client B
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
+ executor.run_until_parked();
+
+ // Verify that following into 1 did not reorder
+ assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let executor = cx_a.executor();
+ 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;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ // Client A shares a project.
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ "4.txt": "four",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Client B joins the project.
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ // Client A opens a file.
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B opens a different file.
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Clients A and B follow each other in split panes
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+ });
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.follow(client_b.peer_id().unwrap(), cx)
+ });
+ executor.run_until_parked();
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+ });
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.follow(client_a.peer_id().unwrap(), cx)
+ });
+ executor.run_until_parked();
+
+ // Clients A and B return focus to the original files they had open
+ workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ executor.run_until_parked();
+
+ // Both clients see the other client's focused file in their right pane.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "1.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_b.peer_id(),
+ items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "2.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
+ },
+ ]
+ );
+
+ // Clients A and B each open a new file.
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "4.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ executor.run_until_parked();
+
+ // Both client's see the other client open the new file, but keep their
+ // focus on their own active pane.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (true, "3.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client A focuses their right pane, in which they're following client B.
+ workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+ executor.run_until_parked();
+
+ // Client B sees that client A is now looking at the same file as them.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (false, "3.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client B focuses their right pane, in which they're following client A,
+ // who is following them.
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ executor.run_until_parked();
+
+ // Client A sees that client B is now looking at the same file as them.
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (false, "3.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client B focuses a file that they previously followed A to, breaking
+ // the follow.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
+ });
+ executor.run_until_parked();
+
+ // Both clients see that client B is looking at that previous file.
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (true, "3.txt".into()),
+ (false, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (true, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+
+ // Client B closes tabs, some of which were originally opened by client A,
+ // and some of which were originally opened by client B.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.close_inactive_items(&Default::default(), cx)
+ .unwrap()
+ .detach();
+ });
+ });
+
+ executor.run_until_parked();
+
+ // Both clients see that Client B is looking at the previous tab.
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "3.txt".into()),]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (true, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+
+ // Client B follows client A again.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.follow(client_a.peer_id().unwrap(), cx)
+ });
+ executor.run_until_parked();
+ // Client A cycles through some tabs.
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
+ });
+ executor.run_until_parked();
+
+ // Client B follows client A into those tabs.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
+ },
+ ]
+ );
+
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
+ });
+ executor.run_until_parked();
+
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "1.txt".into()),
+ (true, "2.txt".into()),
+ (false, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "3.txt".into()),
+ (false, "4.txt".into()),
+ (true, "2.txt".into())
+ ]
+ },
+ ]
+ );
+
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
+ });
+ executor.run_until_parked();
+
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (true, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "3.txt".into()),
+ (false, "4.txt".into()),
+ (false, "2.txt".into()),
+ (true, "1.txt".into()),
+ ]
+ },
+ ]
+ );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ // 2 clients connect to a server.
+ let executor = cx_a.executor();
+ 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;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ // Client A shares a project.
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "1.txt": "one",
+ "2.txt": "two",
+ "3.txt": "three",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ 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 _editor_a1 = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client B starts following client A.
+ let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+ let leader_id = project_b.update(cx_b, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+ let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+
+ // When client B moves, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B edits, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B scrolls, it automatically stops following client A.
+ editor_b2.update(cx_b, |editor, cx| {
+ editor.set_scroll_position(point(0., 3.), cx)
+ });
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+ executor.run_until_parked();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B activates a different pane, it continues following client A in the original pane.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
+ });
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+
+ // When client B activates a different item in the original pane, it automatically stops following client A.
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_simultaneously_following_each_other(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let executor = cx_a.executor();
+ 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;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a.fs().insert_tree("/a", json!({})).await;
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+ executor.run_until_parked();
+ let client_a_id = project_b.update(cx_b, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+ let client_b_id = project_a.update(cx_a, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+
+ workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx));
+ executor.run_until_parked();
+
+ workspace_a.update(cx_a, |workspace, _| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_b_id)
+ );
+ });
+ workspace_b.update(cx_b, |workspace, _| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_a_id)
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ // a and b join a channel/call
+ // a shares project 1
+ // b shares project 2
+ //
+ // b follows a: causes project 2 to be joined, and b to follow a.
+ // b opens a different file in project 2, a follows b
+ // b opens a different file in project 1, a cannot follow b
+ // b shares the project, a joins the project and follows b
+ let executor = cx_a.executor();
+ 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;
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "w.rs": "",
+ "x.rs": "",
+ }),
+ )
+ .await;
+
+ client_b
+ .fs()
+ .insert_tree(
+ "/b",
+ json!({
+ "y.rs": "",
+ "z.rs": "",
+ }),
+ )
+ .await;
+
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
+ let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).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);
+
+ cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
+ cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
+
+ active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+ assert_eq!(visible_push_notifications(cx_b).len(), 1);
+
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.follow(client_a.peer_id().unwrap(), cx)
+ });
+
+ executor.run_until_parked();
+ let window_b_project_a = cx_b
+ .windows()
+ .iter()
+ .max_by_key(|window| window.window_id())
+ .unwrap()
+ .clone();
+
+ let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b);
+
+ let workspace_b_project_a = window_b_project_a
+ .downcast::<Workspace>()
+ .unwrap()
+ .root(cx_b)
+ .unwrap();
+
+ // assert that b is following a in project a in w.rs
+ workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
+ assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
+ assert_eq!(
+ client_a.peer_id(),
+ workspace.leader_for_pane(workspace.active_pane())
+ );
+ let item = workspace.active_item(cx).unwrap();
+ assert_eq!(
+ item.tab_description(0, cx).unwrap(),
+ SharedString::from("w.rs")
+ );
+ });
+
+ // TODO: in app code, this would be done by the collab_ui.
+ active_call_b
+ .update(&mut cx_b2, |call, cx| {
+ let project = workspace_b_project_a.read(cx).project().clone();
+ call.set_location(Some(&project), cx)
+ })
+ .await
+ .unwrap();
+
+ // assert that there are no share notifications open
+ assert_eq!(visible_push_notifications(cx_b).len(), 0);
+
+ // b moves to x.rs in a's project, and a follows
+ workspace_b_project_a
+ .update(&mut cx_b2, |workspace, cx| {
+ workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+ workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
+ let item = workspace.active_item(cx).unwrap();
+ assert_eq!(
+ item.tab_description(0, cx).unwrap(),
+ SharedString::from("x.rs")
+ );
+ });
+
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.follow(client_b.peer_id().unwrap(), cx)
+ });
+
+ executor.run_until_parked();
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+ assert_eq!(
+ client_b.peer_id(),
+ workspace.leader_for_pane(workspace.active_pane())
+ );
+ let item = workspace.active_pane().read(cx).active_item().unwrap();
+ assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs");
+ });
+
+ // b moves to y.rs in b's project, a is still following but can't yet see
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
+ })
+ .await
+ .unwrap();
+
+ // TODO: in app code, this would be done by the collab_ui.
+ active_call_b
+ .update(cx_b, |call, cx| {
+ let project = workspace_b.read(cx).project().clone();
+ call.set_location(Some(&project), cx)
+ })
+ .await
+ .unwrap();
+
+ let project_b_id = active_call_b
+ .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+ assert_eq!(visible_push_notifications(cx_a).len(), 1);
+ cx_a.update(|cx| {
+ workspace::join_remote_project(
+ project_b_id,
+ client_b.user_id().unwrap(),
+ client_a.app_state.clone(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ assert_eq!(visible_push_notifications(cx_a).len(), 0);
+ let window_a_project_b = cx_a
+ .windows()
+ .iter()
+ .max_by_key(|window| window.window_id())
+ .unwrap()
+ .clone();
+ let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a);
+ let workspace_a_project_b = window_a_project_b
+ .downcast::<Workspace>()
+ .unwrap()
+ .root(cx_a)
+ .unwrap();
+
+ workspace_a_project_b.update(cx_a2, |workspace, cx| {
+ assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
+ assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+ assert_eq!(
+ client_b.peer_id(),
+ workspace.leader_for_pane(workspace.active_pane())
+ );
+ let item = workspace.active_item(cx).unwrap();
+ assert_eq!(
+ item.tab_description(0, cx).unwrap(),
+ SharedString::from("y.rs")
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_following_into_excluded_file(
+ mut cx_a: &mut TestAppContext,
+ mut cx_b: &mut TestAppContext,
+) {
+ let executor = cx_a.executor();
+ 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;
+ for cx in [&mut cx_a, &mut cx_b] {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+ });
+ });
+ });
+ }
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ 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(
+ "/a",
+ json!({
+ ".git": {
+ "COMMIT_EDITMSG": "write your commit message here",
+ },
+ "1.txt": "one\none\none",
+ "2.txt": "two\ntwo\ntwo",
+ "3.txt": "three\nthree\nthree",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ 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);
+
+ // Client A opens editors for a regular file and an excluded file.
+ let editor_for_regular = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let editor_for_excluded_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client A updates their selections in those editors
+ editor_for_regular.update(cx_a, |editor, cx| {
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+ });
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ editor.select_all(&Default::default(), cx);
+ editor.handle_input("new commit message", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+ });
+
+ // When client B starts following client A, currently visible file is replicated
+ workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
+ executor.run_until_parked();
+
+ let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+ Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+ );
+ assert_eq!(
+ editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![18..17]
+ );
+
+ // Changes from B to the excluded file are replicated in A's editor
+ editor_for_excluded_b.update(cx_b, |editor, cx| {
+ editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+ });
+ executor.run_until_parked();
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ "new commit messag\nCo-Authored-By: B <b@b.b>"
+ );
+ });
+}
+
+fn visible_push_notifications(
+ cx: &mut TestAppContext,
+) -> Vec<gpui::View<ProjectSharedNotification>> {
+ let mut ret = Vec::new();
+ for window in cx.windows() {
+ window
+ .update(cx, |window, _| {
+ if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
+ ret.push(handle)
+ }
+ })
+ .unwrap();
+ }
+ ret
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+ active: bool,
+ leader: Option<PeerId>,
+ items: Vec<(bool, String)>,
+}
+
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+ cx.read(|cx| {
+ let active_call = ActiveCall::global(cx).read(cx);
+ let peer_id = active_call.client().peer_id();
+ let room = active_call.room().unwrap().read(cx);
+ let mut result = room
+ .remote_participants()
+ .values()
+ .map(|participant| participant.peer_id)
+ .chain(peer_id)
+ .filter_map(|peer_id| {
+ let followers = room.followers_for(peer_id, project_id);
+ if followers.is_empty() {
+ None
+ } else {
+ Some((peer_id, followers.to_vec()))
+ }
+ })
+ .collect::<Vec<_>>();
+ result.sort_by_key(|e| e.0);
+ result
+ })
+}
+
+fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
+ workspace.update(cx, |workspace, cx| {
+ let active_pane = workspace.active_pane();
+ workspace
+ .panes()
+ .iter()
+ .map(|pane| {
+ let leader = workspace.leader_for_pane(pane);
+ let active = pane == active_pane;
+ let pane = pane.read(cx);
+ let active_ix = pane.active_item_index();
+ PaneSummary {
+ active,
+ leader,
+ items: pane
+ .items()
+ .enumerate()
+ .map(|(ix, item)| {
+ (
+ ix == active_ix,
+ item.tab_description(0, cx)
+ .map_or(String::new(), |s| s.to_string()),
+ )
+ })
+ .collect(),
+ }
+ })
+ .collect()
+ })
+}
@@ -61,6 +61,7 @@ schemars.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
+serde_json.workspace = true
time.workspace = true
smallvec.workspace = true
@@ -7,9 +7,9 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use gpui::{
- actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
- ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
- Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+ actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent,
+ ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render,
+ Subscription, Task, View, ViewContext, VisualContext, WeakView,
};
use language::LanguageRegistry;
use menu::Confirm;
@@ -271,11 +271,12 @@ mod tests {
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
cx.update(|cx| {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init(cx);
@@ -15,12 +15,12 @@ use editor::{Editor, EditorElement, EditorStyle};
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
- AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
- FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset,
- ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
- RenderOnce, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext,
- VisualContext, WeakView, WhiteSpace,
+ actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext,
+ AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
+ FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, ListState,
+ Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce,
+ SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
+ WeakView, WhiteSpace,
};
use menu::{Cancel, Confirm, SelectNext, SelectPrev};
use project::{Fs, Project};
@@ -896,7 +896,7 @@ impl CollabPanel {
.start_slot(
h_stack()
.gap_1()
- .child(render_tree_branch(is_last, cx))
+ .child(render_tree_branch(is_last, false, cx))
.child(IconButton::new(0, Icon::Folder)),
)
.child(Label::new(project_name.clone()))
@@ -917,7 +917,7 @@ impl CollabPanel {
.start_slot(
h_stack()
.gap_1()
- .child(render_tree_branch(is_last, cx))
+ .child(render_tree_branch(is_last, false, cx))
.child(IconButton::new(0, Icon::Screen)),
)
.child(Label::new("Screen"))
@@ -958,7 +958,7 @@ impl CollabPanel {
.start_slot(
h_stack()
.gap_1()
- .child(render_tree_branch(false, cx))
+ .child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, Icon::File)),
)
.child(div().h_7().w_full().child(Label::new("notes")))
@@ -979,7 +979,7 @@ impl CollabPanel {
.start_slot(
h_stack()
.gap_1()
- .child(render_tree_branch(false, cx))
+ .child(render_tree_branch(false, false, cx))
.child(IconButton::new(0, Icon::MessageBubbles)),
)
.child(Label::new("chat"))
@@ -1007,7 +1007,7 @@ impl CollabPanel {
.start_slot(
h_stack()
.gap_1()
- .child(render_tree_branch(!has_visible_participants, cx))
+ .child(render_tree_branch(!has_visible_participants, false, cx))
.child(""),
)
.child(Label::new(if count == 1 {
@@ -2404,11 +2404,11 @@ impl CollabPanel {
}
}
-fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
+fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
let rem_size = cx.rem_size();
let line_height = cx.text_style().line_height_in_pixels(rem_size);
let width = rem_size * 1.5;
- let thickness = px(2.);
+ let thickness = px(1.);
let color = cx.theme().colors().text;
canvas(move |bounds, cx| {
@@ -2422,7 +2422,11 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
point(start_x, top),
point(
start_x + thickness,
- if is_last { start_y } else { bounds.bottom() },
+ if is_last {
+ start_y
+ } else {
+ bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
+ },
),
),
color,
@@ -41,6 +41,7 @@ pub fn init(cx: &mut AppContext) {
workspace.set_titlebar_item(titlebar_item.into(), cx)
})
.detach();
+ // todo!()
// cx.add_action(CollabTitlebarItem::share_project);
// cx.add_action(CollabTitlebarItem::unshare_project);
// cx.add_action(CollabTitlebarItem::toggle_user_menu);
@@ -92,7 +93,7 @@ impl Render for CollabTitlebarItem {
.gap_1()
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
- .children(self.render_project_branch(cx))
+ .child(div().pr_1().children(self.render_project_branch(cx)))
.when_some(
current_user.clone().zip(client.peer_id()).zip(room.clone()),
|this, ((current_user, peer_id), room)| {
@@ -184,6 +185,16 @@ impl Render for CollabTitlebarItem {
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
)
+ .tooltip(move |cx| {
+ Tooltip::text(
+ if is_shared {
+ "Stop sharing project with call participants"
+ } else {
+ "Share project with call participants"
+ },
+ cx,
+ )
+ })
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
@@ -200,14 +211,19 @@ impl Render for CollabTitlebarItem {
)
})
.child(
- IconButton::new("leave-call", ui::Icon::Exit)
- .style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .on_click(move |_, cx| {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- }),
+ div()
+ .child(
+ IconButton::new("leave-call", ui::Icon::Exit)
+ .style(ButtonStyle::Subtle)
+ .tooltip(|cx| Tooltip::text("Leave call", cx))
+ .icon_size(IconSize::Small)
+ .on_click(move |_, cx| {
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.hang_up(cx))
+ .detach_and_log_err(cx);
+ }),
+ )
+ .pr_2(),
)
.when(!read_only, |this| {
this.child(
@@ -219,6 +235,16 @@ impl Render for CollabTitlebarItem {
ui::Icon::Mic
},
)
+ .tooltip(move |cx| {
+ Tooltip::text(
+ if is_muted {
+ "Unmute microphone"
+ } else {
+ "Mute microphone"
+ },
+ cx,
+ )
+ })
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_muted)
@@ -260,11 +286,22 @@ impl Render for CollabTitlebarItem {
.icon_size(IconSize::Small)
.selected(is_screen_sharing)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .tooltip(move |cx| {
+ Tooltip::text(
+ if is_screen_sharing {
+ "Stop Sharing Screen"
+ } else {
+ "Share Screen"
+ },
+ cx,
+ )
+ })
.on_click(move |_, cx| {
crate::toggle_screen_sharing(&Default::default(), cx)
}),
)
})
+ .child(div().pr_2())
})
.map(|el| {
let status = self.client.status();
@@ -284,11 +321,19 @@ impl Render for CollabTitlebarItem {
fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
let color = colors.color_for_participant(participant_index.0).cursor;
canvas(move |bounds, cx| {
- let mut path = Path::new(bounds.lower_left());
let height = bounds.size.height;
- path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
- path.line_to(bounds.upper_right() - point(height, px(0.)));
- path.curve_to(bounds.lower_right(), bounds.upper_right());
+ let horizontal_offset = height;
+ let vertical_offset = px(height.0 / 2.0);
+ let mut path = Path::new(bounds.lower_left());
+ path.curve_to(
+ bounds.origin + point(horizontal_offset, vertical_offset),
+ bounds.origin + point(px(0.0), vertical_offset),
+ );
+ path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
+ path.curve_to(
+ bounds.lower_right(),
+ bounds.upper_right() + point(px(0.0), vertical_offset),
+ );
path.line_to(bounds.lower_left());
cx.paint_path(path, color);
})
@@ -58,7 +58,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
room.id(),
room.channel_id(),
&client,
- cx,
);
Task::ready(room.unshare_screen(cx))
} else {
@@ -67,7 +66,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
room.id(),
room.channel_id(),
&client,
- cx,
);
room.share_screen(cx)
}
@@ -86,7 +84,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
} else {
"disable microphone"
};
- report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+ report_call_event_for_room(operation, room.id(), room.channel_id(), &client);
room.toggle_mute(cx)
})
@@ -6,11 +6,11 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
- actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
- CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
- InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
- ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
- VisualContext, WeakView, WindowContext,
+ actions, div, img, list, px, AnyElement, AppContext, AsyncWindowContext, CursorStyle,
+ DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+ IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render,
+ StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
};
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs;
@@ -24,19 +24,11 @@ impl Render for DiagnosticIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().map(|this| {
- if !self.in_progress_checks.is_empty() {
- this.child(
- IconElement::new(Icon::ArrowCircle)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- } else {
- this.child(
- IconElement::new(Icon::Check)
- .size(IconSize::Small)
- .color(Color::Default),
- )
- }
+ this.child(
+ IconElement::new(Icon::Check)
+ .size(IconSize::Small)
+ .color(Color::Default),
+ )
}),
(0, warning_count) => h_stack()
.gap_1()
@@ -72,9 +64,14 @@ impl Render for DiagnosticIndicator {
let status = if !self.in_progress_checks.is_empty() {
Some(
- Label::new("Checking…")
- .size(LabelSize::Small)
- .color(Color::Muted)
+ h_stack()
+ .gap_2()
+ .child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
+ .child(
+ Label::new("Checking…")
+ .size(LabelSize::Small)
+ .into_any_element(),
+ )
.into_any_element(),
)
} else if let Some(diagnostic) = &self.current_diagnostic {
@@ -240,7 +240,7 @@ impl DisplayMap {
}
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
- cleared |= self.inlay_highlights.remove(&type_id).is_none();
+ cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@@ -8874,7 +8874,7 @@ impl Editor {
let telemetry = project.read(cx).client().telemetry().clone();
- telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension, cx)
+ telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
}
#[cfg(any(test, feature = "test-support"))]
@@ -8926,7 +8926,6 @@ impl Editor {
operation,
copilot_enabled,
copilot_enabled_for_language,
- cx,
)
}
@@ -9,11 +9,7 @@ use crate::{
};
use futures::StreamExt;
-use gpui::{
- div,
- serde_json::{self, json},
- TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
-};
+use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
@@ -24,6 +20,7 @@ use language::{
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
+use serde_json::{self, json};
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
@@ -8131,8 +8128,8 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
/// Handle completion request passing a marked string specifying where the completion
/// should be triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range
-pub fn handle_completion_request<'a>(
- cx: &mut EditorLspTestContext<'a>,
+pub fn handle_completion_request(
+ cx: &mut EditorLspTestContext,
marked_string: &str,
completions: Vec<&'static str>,
) -> impl Future<Output = ()> {
@@ -8177,8 +8174,8 @@ pub fn handle_completion_request<'a>(
}
}
-fn handle_resolve_completion_request<'a>(
- cx: &mut EditorLspTestContext<'a>,
+fn handle_resolve_completion_request(
+ cx: &mut EditorLspTestContext,
edits: Option<Vec<(&'static str, &'static str)>>,
) -> impl Future<Output = ()> {
let edits = edits.map(|edits| {
@@ -795,7 +795,7 @@ impl EditorElement {
cx.paint_quad(quad(
highlight_bounds,
Corners::all(1. * line_height),
- gpui::yellow(), // todo!("use the right color")
+ cx.theme().status().modified,
Edges::default(),
transparent_black(),
));
@@ -21,19 +21,19 @@ use workspace::{AppState, Workspace, WorkspaceHandle};
use super::editor_test_context::{AssertionContextManager, EditorTestContext};
-pub struct EditorLspTestContext<'a> {
- pub cx: EditorTestContext<'a>,
+pub struct EditorLspTestContext {
+ pub cx: EditorTestContext,
pub lsp: lsp::FakeLanguageServer,
pub workspace: View<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
-impl<'a> EditorLspTestContext<'a> {
+impl EditorLspTestContext {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
- cx: &'a mut gpui::TestAppContext,
- ) -> EditorLspTestContext<'a> {
+ cx: &mut gpui::TestAppContext,
+ ) -> EditorLspTestContext {
let app_state = cx.update(AppState::test);
cx.update(|cx| {
@@ -110,8 +110,8 @@ impl<'a> EditorLspTestContext<'a> {
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
- cx: &'a mut gpui::TestAppContext,
- ) -> EditorLspTestContext<'a> {
+ cx: &mut gpui::TestAppContext,
+ ) -> EditorLspTestContext {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
@@ -152,8 +152,8 @@ impl<'a> EditorLspTestContext<'a> {
pub async fn new_typescript(
capabilities: lsp::ServerCapabilities,
- cx: &'a mut gpui::TestAppContext,
- ) -> EditorLspTestContext<'a> {
+ cx: &mut gpui::TestAppContext,
+ ) -> EditorLspTestContext {
let mut word_characters: HashSet<char> = Default::default();
word_characters.insert('$');
word_characters.insert('#');
@@ -283,15 +283,15 @@ impl<'a> EditorLspTestContext<'a> {
}
}
-impl<'a> Deref for EditorLspTestContext<'a> {
- type Target = EditorTestContext<'a>;
+impl Deref for EditorLspTestContext {
+ type Target = EditorTestContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
-impl<'a> DerefMut for EditorLspTestContext<'a> {
+impl DerefMut for EditorLspTestContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
@@ -26,20 +26,20 @@ use util::{
use super::build_editor_with_project;
-pub struct EditorTestContext<'a> {
- pub cx: gpui::VisualTestContext<'a>,
+pub struct EditorTestContext {
+ pub cx: gpui::VisualTestContext,
pub window: AnyWindowHandle,
pub editor: View<Editor>,
pub assertion_cx: AssertionContextManager,
}
-impl<'a> EditorTestContext<'a> {
- pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+impl EditorTestContext {
+ pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
let fs = FakeFs::new(cx.executor());
// fs.insert_file("/file", "".to_owned()).await;
fs.insert_tree(
"/root",
- gpui::serde_json::json!({
+ serde_json::json!({
"file": "",
}),
)
@@ -342,7 +342,7 @@ impl<'a> EditorTestContext<'a> {
}
}
-impl<'a> Deref for EditorTestContext<'a> {
+impl Deref for EditorTestContext {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
@@ -350,7 +350,7 @@ impl<'a> Deref for EditorTestContext<'a> {
}
}
-impl<'a> DerefMut for EditorTestContext<'a> {
+impl DerefMut for EditorTestContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
@@ -36,6 +36,7 @@ postage.workspace = true
regex.workspace = true
serde.workspace = true
serde_derive.workspace = true
+serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
sysinfo.workspace = true
@@ -7,8 +7,8 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent};
use futures::AsyncReadExt;
use gpui::{
- div, red, rems, serde_json, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
- Model, PromptLevel, Render, Task, View, ViewContext,
+ div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+ PromptLevel, Render, Task, View, ViewContext,
};
use isahc::Request;
use language::Buffer;
@@ -505,8 +505,7 @@ impl FileFinderDelegate {
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
- let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
- - file_name.chars().count();
+ let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
@@ -819,6 +818,44 @@ mod tests {
}
}
+ #[gpui::test]
+ async fn test_complex_path(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "其他": {
+ "S数据表格": {
+ "task.xlsx": "some content",
+ },
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+
+ let (picker, workspace, cx) = build_find_picker(project, cx);
+
+ cx.simulate_input("t");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 1);
+ assert_eq!(
+ collect_search_results(picker),
+ vec![PathBuf::from("其他/S数据表格/task.xlsx")],
+ )
+ });
+ 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), "task.xlsx");
+ });
+ }
+
#[gpui::test]
async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -1843,7 +1880,7 @@ mod tests {
expected_matches: usize,
expected_editor_title: &str,
workspace: &View<Workspace>,
- cx: &mut gpui::VisualTestContext<'_>,
+ cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
let picker = open_file_picker(&workspace, cx);
cx.simulate_input(input);
@@ -29,7 +29,7 @@ use std::any::{Any, TypeId};
/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
-/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
+/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
/// pub struct Paste {
/// pub content: SharedString,
/// }
@@ -158,12 +158,12 @@ impl ActionRegistry {
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
- #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
- #[serde(crate = "gpui::serde")]
+ #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)]
+ #[serde(crate = "gpui::private::serde")]
pub struct $name;
gpui::__impl_action!($namespace, $name,
- fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+ fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
}
);
@@ -179,8 +179,8 @@ macro_rules! impl_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
gpui::__impl_action!($namespace, $name,
- fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
- Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+ fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+ Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
}
);
@@ -43,7 +43,7 @@ use util::{
ResultExt,
};
-/// Temporary(?) wrapper around RefCell<AppContext> to help us debug any double borrows.
+/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
/// Strongly consider removing after stabilization.
pub struct AppCell {
app: RefCell<AppContext>,
@@ -964,7 +964,7 @@ impl AppContext {
/// Event handlers propagate events by default. Call this method to stop dispatching to
/// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is
- /// the opposite of [propagate]. It's also possible to cancel a call to [propagate] by
+ /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by
/// calling this method before effects are flushed.
pub fn stop_propagation(&mut self) {
self.propagate_event = false;
@@ -972,7 +972,7 @@ impl AppContext {
/// Action handlers stop propagation by default during the bubble phase of action dispatch
/// dispatching to action handlers higher in the element tree. This is the opposite of
- /// [stop_propagation]. It's also possible to cancel a call to [stop_propagate] by calling
+ /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling
/// this method before effects are flushed.
pub fn propagate(&mut self) {
self.propagate_event = true;
@@ -1099,12 +1099,6 @@ impl AppContext {
pub fn has_active_drag(&self) -> bool {
self.active_drag.is_some()
}
-
- pub fn active_drag<T: 'static>(&self) -> Option<&T> {
- self.active_drag
- .as_ref()
- .and_then(|drag| drag.value.downcast_ref())
- }
}
impl Context for AppContext {
@@ -1,4 +1,4 @@
-use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
+use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
@@ -187,6 +187,10 @@ impl TestAppContext {
self.test_window(window_handle).simulate_resize(size);
}
+ pub fn windows(&self) -> Vec<AnyWindowHandle> {
+ self.app.borrow().windows().clone()
+ }
+
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
@@ -479,21 +483,24 @@ impl<V> View<V> {
}
use derive_more::{Deref, DerefMut};
-#[derive(Deref, DerefMut)]
-pub struct VisualTestContext<'a> {
+#[derive(Deref, DerefMut, Clone)]
+pub struct VisualTestContext {
#[deref]
#[deref_mut]
- cx: &'a mut TestAppContext,
+ cx: TestAppContext,
window: AnyWindowHandle,
}
-impl<'a> VisualTestContext<'a> {
+impl<'a> VisualTestContext {
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
}
- pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
- Self { cx, window }
+ pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
+ Self {
+ cx: cx.clone(),
+ window,
+ }
}
pub fn run_until_parked(&self) {
@@ -525,9 +532,36 @@ impl<'a> VisualTestContext<'a> {
}
self.background_executor.run_until_parked();
}
+ /// Returns true if the window was closed.
+ pub fn simulate_close(&mut self) -> bool {
+ let handler = self
+ .cx
+ .update_window(self.window, |_, cx| {
+ cx.window
+ .platform_window
+ .as_test()
+ .unwrap()
+ .0
+ .lock()
+ .should_close_handler
+ .take()
+ })
+ .unwrap();
+ if let Some(mut handler) = handler {
+ let should_close = handler();
+ self.cx
+ .update_window(self.window, |_, cx| {
+ cx.window.platform_window.on_should_close(handler);
+ })
+ .unwrap();
+ should_close
+ } else {
+ false
+ }
+ }
}
-impl<'a> Context for VisualTestContext<'a> {
+impl Context for VisualTestContext {
type Result<T> = <TestAppContext as Context>::Result<T>;
fn new_model<T: 'static>(
@@ -578,7 +612,7 @@ impl<'a> Context for VisualTestContext<'a> {
}
}
-impl<'a> VisualContext for VisualTestContext<'a> {
+impl VisualContext for VisualTestContext {
fn new_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@@ -587,7 +621,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
V: 'static + Render,
{
self.window
- .update(self.cx, |_, cx| cx.new_view(build_view))
+ .update(&mut self.cx, |_, cx| cx.new_view(build_view))
.unwrap()
}
@@ -597,7 +631,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R> {
self.window
- .update(self.cx, |_, cx| cx.update_view(view, update))
+ .update(&mut self.cx, |_, cx| cx.update_view(view, update))
.unwrap()
}
@@ -609,13 +643,13 @@ impl<'a> VisualContext for VisualTestContext<'a> {
V: 'static + Render,
{
self.window
- .update(self.cx, |_, cx| cx.replace_root_view(build_view))
+ .update(&mut self.cx, |_, cx| cx.replace_root_view(build_view))
.unwrap()
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.window
- .update(self.cx, |_, cx| {
+ .update(&mut self.cx, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx)
})
.unwrap()
@@ -626,7 +660,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
V: crate::ManagedView,
{
self.window
- .update(self.cx, |_, cx| {
+ .update(&mut self.cx, |_, cx| {
view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
})
.unwrap()
@@ -1582,7 +1582,6 @@ impl From<f32> for Edges<Pixels> {
/// Represents the corners of a box in a 2D space, such as border radius.
///
/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
-/// ```
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
#[refineable(Debug)]
#[repr(C)]
@@ -2263,7 +2262,7 @@ impl From<f64> for GlobalPixels {
}
}
-/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [WindowContext::set_rem_size].
+/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`WindowContext::set_rem_size`][set_rem_size].
///
/// Rems are used for defining lengths that are scalable and consistent across different UI elements.
/// The value of `1rem` is typically equal to the font-size of the root element (often the `<html>` element in browsers),
@@ -2271,6 +2270,8 @@ impl From<f64> for GlobalPixels {
/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences.
///
/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`.
+///
+/// [set_rem_size]: crate::WindowContext::set_rem_size
#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)]
pub struct Rems(pub f32);
@@ -30,7 +30,16 @@ mod util;
mod view;
mod window;
-mod private {
+/// Do not touch, here be dragons for use by gpui_macros and such.
+#[doc(hidden)]
+pub mod private {
+ pub use linkme;
+ pub use serde;
+ pub use serde_derive;
+ pub use serde_json;
+}
+
+mod seal {
/// A mechanism for restricting implementations of a trait to only those in GPUI.
/// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
pub trait Sealed {}
@@ -47,22 +56,17 @@ pub use element::*;
pub use elements::*;
pub use executor::*;
pub use geometry::*;
-pub use gpui_macros::*;
+pub use gpui_macros::{register_action, test, IntoElement, Render};
pub use image_cache::*;
pub use input::*;
pub use interactive::*;
pub use key_dispatch::*;
pub use keymap::*;
-pub use linkme;
pub use platform::*;
-use private::Sealed;
pub use refineable::*;
pub use scene::*;
-pub use serde;
-pub use serde_derive;
-pub use serde_json;
+use seal::Sealed;
pub use shared_string::*;
-pub use smallvec;
pub use smol::Timer;
pub use style::*;
pub use styled::*;
@@ -5,8 +5,8 @@ use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
///
-/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler<V>].
-/// This input handler can then be assigned during paint by calling [WindowContext::handle_input].
+/// Once your view `V` implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
+/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`].
pub trait InputHandler: 'static + Sized {
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
-> Option<String>;
@@ -43,8 +43,10 @@ pub struct ElementInputHandler<V> {
}
impl<V: 'static> ElementInputHandler<V> {
- /// Used in [Element::paint] with the element's bounds and a view context for its
+ /// Used in [`Element::paint`][element_paint] with the element's bounds and a view context for its
/// containing view.
+ ///
+ /// [element_paint]: crate::Element::paint
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
ElementInputHandler {
view,
@@ -214,7 +214,7 @@ impl Render for ExternalPaths {
pub enum FileDropEvent {
Entered {
position: Point<Pixels>,
- files: ExternalPaths,
+ paths: ExternalPaths,
},
Pending {
position: Point<Pixels>,
@@ -37,7 +37,7 @@ pub use keystroke::*;
pub use mac::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
-pub use time::UtcOffset;
+use time::UtcOffset;
#[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
@@ -106,11 +106,6 @@ impl From<NSSize> for Size<Pixels> {
}
}
-pub trait NSRectExt {
- fn size(&self) -> Size<Pixels>;
- fn intersects(&self, other: Self) -> bool;
-}
-
impl From<NSRect> for Size<Pixels> {
fn from(rect: NSRect) -> Self {
let NSSize { width, height } = rect.size;
@@ -124,16 +119,3 @@ impl From<NSRect> for Size<GlobalPixels> {
size(width.into(), height.into())
}
}
-
-// impl NSRectExt for NSRect {
-// fn intersects(&self, other: Self) -> bool {
-// self.size.width > 0.
-// && self.size.height > 0.
-// && other.size.width > 0.
-// && other.size.height > 0.
-// && self.origin.x <= other.origin.x + other.size.width
-// && self.origin.x + self.size.width >= other.origin.x
-// && self.origin.y <= other.origin.y + other.size.height
-// && self.origin.y + self.size.height >= other.origin.y
-// }
-// }
@@ -13,9 +13,14 @@ use parking::{Parker, Unparker};
use parking_lot::Mutex;
use std::{ffi::c_void, ptr::NonNull, sync::Arc, time::Duration};
-include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
+/// these pub items from leaking into public API.
+pub(crate) mod dispatch_sys {
+ include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+}
-pub fn dispatch_get_main_queue() -> dispatch_queue_t {
+use dispatch_sys::*;
+pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
}
@@ -51,7 +51,7 @@ impl MacDisplay {
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
- pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
+ fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
@@ -94,7 +94,7 @@ unsafe extern "C" fn trampoline(
mod sys {
//! Derived from display-link crate under the fololwing license:
- //! https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT
+ //! <https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT>
//! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
#![allow(dead_code, non_upper_case_globals)]
@@ -56,9 +56,6 @@ use time::UtcOffset;
#[allow(non_upper_case_globals)]
const NSUTF8StringEncoding: NSUInteger = 4;
-#[allow(non_upper_case_globals)]
-pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
-
const MAC_PLATFORM_IVAR: &str = "platform";
static mut APP_CLASS: *const Class = ptr::null();
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
@@ -404,7 +401,7 @@ impl Platform for MacPlatform {
// this, we make quitting the application asynchronous so that we aren't holding borrows to
// the app state on the stack when we actually terminate the app.
- use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
+ use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::dispatch_async_f};
unsafe {
dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
@@ -500,9 +500,9 @@ impl<'a> StringIndexConverter<'a> {
}
#[repr(C)]
-pub struct __CFTypesetter(c_void);
+pub(crate) struct __CFTypesetter(c_void);
-pub type CTTypesetterRef = *const __CFTypesetter;
+type CTTypesetterRef = *const __CFTypesetter;
#[link(name = "CoreText", kind = "framework")]
extern "C" {
@@ -1673,10 +1673,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
if send_new_event(&window_state, {
let position = drag_event_position(&window_state, dragging_info);
let paths = external_paths_from_event(dragging_info);
- InputEvent::FileDrop(FileDropEvent::Entered {
- position,
- files: paths,
- })
+ InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
}) {
NSDragOperationCopy
} else {
@@ -266,7 +266,7 @@ impl Platform for TestPlatform {
}
fn local_timezone(&self) -> time::UtcOffset {
- unimplemented!()
+ time::UtcOffset::UTC
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
@@ -18,7 +18,7 @@ pub struct TestWindowState {
pub(crate) edited: bool,
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>,
-
+ pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
input_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
@@ -44,7 +44,7 @@ impl TestWindow {
sprite_atlas: Arc::new(TestAtlas::new()),
title: Default::default(),
edited: false,
-
+ should_close_handler: None,
input_callback: None,
active_status_change_callback: None,
resize_callback: None,
@@ -117,6 +117,9 @@ impl TestWindow {
self.0.lock().input_handler = Some(input_handler);
}
+ pub fn edited(&self) -> bool {
+ self.0.lock().edited
+ }
}
impl PlatformWindow for TestWindow {
@@ -235,8 +238,8 @@ impl PlatformWindow for TestWindow {
self.0.lock().moved_callback = Some(callback)
}
- fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
- unimplemented!()
+ fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
+ self.0.lock().should_close_handler = Some(callback);
}
fn on_close(&self, _callback: Box<dyn FnOnce()>) {
@@ -561,6 +561,12 @@ impl From<Hsla> for Fill {
}
}
+impl From<Rgba> for Fill {
+ fn from(color: Rgba) -> Self {
+ Self::Color(color.into())
+ }
+}
+
impl From<TextStyle> for HighlightStyle {
fn from(other: TextStyle) -> Self {
Self::from(&other)
@@ -33,7 +33,7 @@ pub struct FontId(pub usize);
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub struct FontFamilyId(pub usize);
-pub const SUBPIXEL_VARIANTS: u8 = 4;
+pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
pub struct TextSystem {
line_layout_cache: Arc<LineLayoutCache>,
@@ -1,12 +1,12 @@
use crate::{
- private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
+ seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
WindowContext,
};
use anyhow::{Context, Result};
use std::{
- any::TypeId,
+ any::{type_name, TypeId},
fmt,
hash::{Hash, Hasher},
};
@@ -104,6 +104,14 @@ impl<V> Clone for View<V> {
}
}
+impl<T> std::fmt::Debug for View<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct(&format!("View<{}>", type_name::<T>()))
+ .field("entity_id", &self.model.entity_id)
+ .finish_non_exhaustive()
+ }
+}
+
impl<V> Hash for View<V> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.model.hash(state);
@@ -1462,12 +1462,12 @@ impl<'a> WindowContext<'a> {
// Translate dragging and dropping of external files from the operating system
// to internal drag and drop events.
InputEvent::FileDrop(file_drop) => match file_drop {
- FileDropEvent::Entered { position, files } => {
+ FileDropEvent::Entered { position, paths } => {
self.window.mouse_position = position;
if self.active_drag.is_none() {
self.active_drag = Some(AnyDrag {
- value: Box::new(files.clone()),
- view: self.new_view(|_| files).into(),
+ value: Box::new(paths.clone()),
+ view: self.new_view(|_| paths).into(),
cursor_offset: position,
});
}
@@ -1826,9 +1826,11 @@ impl<'a> WindowContext<'a> {
result
}
- /// Set an input handler, such as [ElementInputHandler], which interfaces with the
+ /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
/// platform to receive textual input with proper integration with concerns such
/// as IME interactions.
+ ///
+ /// [element_input_handler]: crate::ElementInputHandler
pub fn handle_input(
&mut self,
focus_handle: &FocusHandle,
@@ -2500,8 +2502,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Register a listener to be called when the given focus handle receives focus.
- /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
- /// is dropped.
+ /// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus(
&mut self,
handle: &FocusHandle,
@@ -2527,8 +2528,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Register a listener to be called when the given focus handle or one of its descendants receives focus.
- /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
- /// is dropped.
+ /// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_in(
&mut self,
handle: &FocusHandle,
@@ -2554,8 +2554,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Register a listener to be called when the given focus handle loses focus.
- /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
- /// is dropped.
+ /// Returns a subscription and persists until the subscription is dropped.
pub fn on_blur(
&mut self,
handle: &FocusHandle,
@@ -2581,8 +2580,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Register a listener to be called when the window loses focus.
- /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
- /// is dropped.
+ /// Returns a subscription and persists until the subscription is dropped.
pub fn on_blur_window(
&mut self,
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
@@ -2597,8 +2595,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
- /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
- /// is dropped.
+ /// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out(
&mut self,
handle: &FocusHandle,
@@ -11,7 +11,7 @@ fn test_action_macros() {
impl_actions!(test, [AnotherTestAction]);
- #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+ #[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
struct RegisterableAction {}
register_action!(RegisterableAction);
@@ -36,8 +36,8 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
quote! {
#[doc(hidden)]
- #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
- #[linkme(crate = gpui::linkme)]
+ #[gpui::private::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+ #[linkme(crate = gpui::private::linkme)]
static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
/// This is an auto generated function, do not use.
@@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use workspace::{AppState, Workspace};
+use workspace::{AppState, OpenVisible, Workspace};
actions!(journal, [NewJournalEntry]);
@@ -100,7 +100,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
let opened = workspace
.update(&mut cx, |workspace, cx| {
- workspace.open_paths(vec![entry_path], true, cx)
+ workspace.open_paths(vec![entry_path], OpenVisible::All, None, cx)
})?
.await;
@@ -24,6 +24,7 @@ futures.workspace = true
serde.workspace = true
anyhow.workspace = true
tree-sitter.workspace = true
+serde_json.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
@@ -4,9 +4,10 @@ use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
-use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext};
+use gpui::{Context, TestAppContext, VisualTestContext};
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
use project::{FakeFs, Project};
+use serde_json::json;
use settings::SettingsStore;
#[gpui::test]
@@ -24,6 +24,7 @@ anyhow.workspace = true
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
+serde_json.workspace = true
[dev-dependencies]
futures.workspace = true
@@ -260,9 +260,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
mod tests {
use super::*;
use futures::StreamExt;
- use gpui::{serde_json::json, TestAppContext, VisualContext};
+ use gpui::{TestAppContext, VisualContext};
use language::{FakeLspAdapter, Language, LanguageConfig};
use project::FakeFs;
+ use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
@@ -1091,13 +1091,10 @@ mod tests {
theme::init(theme::LoadThemes::JustBase, cx);
});
}
+
fn init_test(
cx: &mut TestAppContext,
- ) -> (
- View<Editor>,
- View<BufferSearchBar>,
- &mut VisualTestContext<'_>,
- ) {
+ ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
init_globals(cx);
let buffer = cx.new_model(|cx| {
Buffer::new(
@@ -1311,34 +1311,33 @@ impl Terminal {
})
}
- pub fn title(&self) -> String {
+ pub fn title(&self, truncate: bool) -> String {
self.foreground_process_info
.as_ref()
.map(|fpi| {
- format!(
- "{} — {}",
- truncate_and_trailoff(
- &fpi.cwd
- .file_name()
- .map(|name| name.to_string_lossy().to_string())
- .unwrap_or_default(),
- 25
- ),
- truncate_and_trailoff(
- &{
- format!(
- "{}{}",
- fpi.name,
- if fpi.argv.len() >= 1 {
- format!(" {}", (&fpi.argv[1..]).join(" "))
- } else {
- "".to_string()
- }
- )
- },
- 25
+ let process_file = fpi
+ .cwd
+ .file_name()
+ .map(|name| name.to_string_lossy().to_string())
+ .unwrap_or_default();
+ let process_name = format!(
+ "{}{}",
+ fpi.name,
+ if fpi.argv.len() >= 1 {
+ format!(" {}", (&fpi.argv[1..]).join(" "))
+ } else {
+ "".to_string()
+ }
+ );
+ let (process_file, process_name) = if truncate {
+ (
+ truncate_and_trailoff(&process_file, 25),
+ truncate_and_trailoff(&process_name, 25),
)
- )
+ } else {
+ (process_file, process_name)
+ };
+ format!("{process_file} — {process_name}")
})
.unwrap_or_else(|| "Terminal".to_string())
}
@@ -36,6 +36,7 @@ thiserror.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_derive.workspace = true
+serde_json.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
@@ -1,12 +1,12 @@
use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
use gpui::{
div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
- BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
- FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement,
+ BorrowWindow, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle,
+ FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement,
InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext,
ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point,
- ShapedLine, StatefulInteractiveElement, StyleRefinement, Styled, TextRun, TextStyle,
- TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
+ ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle,
+ WhiteSpace, WindowContext,
};
use itertools::Itertools;
use language::CursorShape;
@@ -25,7 +25,7 @@ use terminal::{
use theme::{ActiveTheme, Theme, ThemeSettings};
use ui::Tooltip;
-use std::{any::TypeId, mem};
+use std::mem;
use std::{fmt::Debug, ops::RangeInclusive};
///The information generated during layout that is necessary for painting
@@ -677,28 +677,6 @@ impl TerminalElement {
}
});
- self.interactivity.drag_over_styles.push((
- TypeId::of::<ExternalPaths>(),
- StyleRefinement::default().bg(cx.theme().colors().drop_target_background),
- ));
- self.interactivity.on_drop::<ExternalPaths>({
- let focus = focus.clone();
- let terminal = terminal.clone();
- move |external_paths, cx| {
- cx.focus(&focus);
- let mut new_text = external_paths
- .paths()
- .iter()
- .map(|path| format!(" {path:?}"))
- .join("");
- new_text.push(' ');
- terminal.update(cx, |terminal, _| {
- // todo!() long paths are not displayed properly albeit the text is there
- terminal.paste(&new_text);
- });
- }
- });
-
// Mouse mode handlers:
// All mouse modes need the extra click handlers
if mode.intersects(TermMode::MOUSE_MODE) {
@@ -1,13 +1,14 @@
-use std::{path::PathBuf, sync::Arc};
+use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
- actions, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
- FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
- Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+ actions, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
+ FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
-use project::Fs;
+use itertools::Itertools;
+use project::{Fs, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -19,7 +20,7 @@ use workspace::{
item::Item,
pane,
ui::Icon,
- Pane, Workspace,
+ DraggedTab, Pane, Workspace,
};
use anyhow::Result;
@@ -59,18 +60,7 @@ impl TerminalPanel {
workspace.weak_handle(),
workspace.project().clone(),
Default::default(),
- Some(Arc::new(|a, cx| {
- if let Some(tab) = a.downcast_ref::<workspace::pane::DraggedTab>() {
- if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) {
- return item.downcast::<TerminalView>().is_some();
- }
- }
- if a.downcast_ref::<ExternalPaths>().is_some() {
- return true;
- }
-
- false
- })),
+ None,
cx,
);
pane.set_can_split(false, cx);
@@ -105,6 +95,52 @@ impl TerminalPanel {
})
.into_any_element()
});
+
+ let workspace = workspace.weak_handle();
+ pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
+ if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
+ let item = if &tab.pane == cx.view() {
+ pane.item_for_index(tab.ix)
+ } else {
+ tab.pane.read(cx).item_for_index(tab.ix)
+ };
+ if let Some(item) = item {
+ if item.downcast::<TerminalView>().is_some() {
+ return ControlFlow::Continue(());
+ } else if let Some(project_path) = item.project_path(cx) {
+ if let Some(entry_path) = workspace
+ .update(cx, |workspace, cx| {
+ workspace
+ .project()
+ .read(cx)
+ .absolute_path(&project_path, cx)
+ })
+ .log_err()
+ .flatten()
+ {
+ add_paths_to_terminal(pane, &[entry_path], cx);
+ }
+ }
+ }
+ } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
+ if let Some(entry_path) = workspace
+ .update(cx, |workspace, cx| {
+ let project = workspace.project().read(cx);
+ project
+ .path_for_entry(entry_id, cx)
+ .and_then(|project_path| project.absolute_path(&project_path, cx))
+ })
+ .log_err()
+ .flatten()
+ {
+ add_paths_to_terminal(pane, &[entry_path], cx);
+ }
+ } else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
+ add_paths_to_terminal(pane, paths.paths(), cx);
+ }
+
+ ControlFlow::Break(())
+ });
let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
pane.toolbar()
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
@@ -217,7 +253,6 @@ impl TerminalPanel {
pane::Event::Remove => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
- pane::Event::Focus => cx.emit(PanelEvent::Focus),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade() {
@@ -330,6 +365,22 @@ impl TerminalPanel {
}
}
+fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
+ if let Some(terminal_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<TerminalView>())
+ {
+ cx.focus_view(&terminal_view);
+ let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
+ new_text.push(' ');
+ terminal_view.update(cx, |terminal_view, cx| {
+ terminal_view.terminal().update(cx, |terminal, _| {
+ terminal.paste(&new_text);
+ });
+ });
+ }
+}
+
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
@@ -29,7 +29,8 @@ use workspace::{
notifications::NotifyResultExt,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
- CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+ CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace,
+ WorkspaceId,
};
use anyhow::Context;
@@ -192,12 +193,26 @@ impl TerminalView {
}
let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
if let Some(path) = potential_abs_paths.into_iter().next() {
- let is_dir = path.path_like.is_dir();
let task_workspace = workspace.clone();
cx.spawn(|_, mut cx| async move {
+ let fs = task_workspace.update(&mut cx, |workspace, cx| {
+ workspace.project().read(cx).fs().clone()
+ })?;
+ let is_dir = fs
+ .metadata(&path.path_like)
+ .await?
+ .with_context(|| {
+ format!("Missing metadata for file {:?}", path.path_like)
+ })?
+ .is_dir;
let opened_items = task_workspace
.update(&mut cx, |workspace, cx| {
- workspace.open_paths(vec![path.path_like], is_dir, cx)
+ workspace.open_paths(
+ vec![path.path_like],
+ OpenVisible::OnlyDirectories,
+ None,
+ cx,
+ )
})
.context("workspace update")?
.await;
@@ -665,7 +680,7 @@ impl Item for TerminalView {
type Event = ItemEvent;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
- Some(self.terminal().read(cx).title().into())
+ Some(self.terminal().read(cx).title(false).into())
}
fn tab_content(
@@ -674,8 +689,7 @@ impl Item for TerminalView {
selected: bool,
cx: &WindowContext,
) -> AnyElement {
- let title = self.terminal().read(cx).title();
-
+ let title = self.terminal().read(cx).title(true);
h_stack()
.gap_2()
.child(IconElement::new(Icon::Terminal))
@@ -18,6 +18,7 @@ palette = { version = "0.7.3", default-features = false, features = ["std"] }
pathfinder_color = "0.5"
rust-embed.workspace = true
serde.workspace = true
+serde_json.workspace = true
simplelog = "0.9"
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme", features = ["importing-themes"] }
@@ -16,7 +16,6 @@ use any_ascii::any_ascii;
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use convert_case::{Case, Casing};
-use gpui::serde_json;
use indexmap::IndexMap;
use indoc::formatdoc;
use json_comments::StripComments;
@@ -1,5 +1,5 @@
use anyhow::{Context, Result};
-use gpui::{serde_json, Hsla, Rgba};
+use gpui::{Hsla, Rgba};
use theme::{
color_alpha, Appearance, PlayerColor, PlayerColors, StatusColorsRefinement,
ThemeColorsRefinement, UserFontStyle, UserFontWeight, UserHighlightStyle, UserSyntaxTheme,
@@ -6,10 +6,10 @@ use std::fmt;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
-use gpui::serde_json::{self, Value};
use pathfinder_color::ColorU;
use serde::de::{self, DeserializeOwned, Unexpected};
use serde::{Deserialize, Deserializer};
+use serde_json::{self, Value};
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
@@ -182,7 +182,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
let theme_name = cx.theme().name.clone();
self.telemetry
- .report_setting_event("theme", theme_name.to_string(), cx);
+ .report_setting_event("theme", theme_name.to_string());
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
settings.theme = Some(theme_name.to_string());
@@ -4,30 +4,27 @@ use crate::state::Mode;
use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
-pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
- cx: NeovimBackedTestContext<'a>,
+pub struct NeovimBackedBindingTestContext<const COUNT: usize> {
+ cx: NeovimBackedTestContext,
keystrokes_under_test: [&'static str; COUNT],
}
-impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
- pub fn new(
- keystrokes_under_test: [&'static str; COUNT],
- cx: NeovimBackedTestContext<'a>,
- ) -> Self {
+impl<const COUNT: usize> NeovimBackedBindingTestContext<COUNT> {
+ pub fn new(keystrokes_under_test: [&'static str; COUNT], cx: NeovimBackedTestContext) -> Self {
Self {
cx,
keystrokes_under_test,
}
}
- pub fn consume(self) -> NeovimBackedTestContext<'a> {
+ pub fn consume(self) -> NeovimBackedTestContext {
self.cx
}
pub fn binding<const NEW_COUNT: usize>(
self,
keystrokes: [&'static str; NEW_COUNT],
- ) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
+ ) -> NeovimBackedBindingTestContext<NEW_COUNT> {
self.consume().binding(keystrokes)
}
@@ -80,15 +77,15 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
}
}
-impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
- type Target = NeovimBackedTestContext<'a>;
+impl<const COUNT: usize> Deref for NeovimBackedBindingTestContext<COUNT> {
+ type Target = NeovimBackedTestContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
-impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
+impl<const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<COUNT> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
@@ -47,8 +47,8 @@ impl ExemptionFeatures {
}
}
-pub struct NeovimBackedTestContext<'a> {
- cx: VimTestContext<'a>,
+pub struct NeovimBackedTestContext {
+ cx: VimTestContext,
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
// bindings are exempted. If None, all bindings are ignored for that insertion text.
exemptions: HashMap<String, Option<HashSet<String>>>,
@@ -60,8 +60,8 @@ pub struct NeovimBackedTestContext<'a> {
is_dirty: bool,
}
-impl<'a> NeovimBackedTestContext<'a> {
- pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
+impl NeovimBackedTestContext {
+ pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
// rust stores the name of the test on the current thread.
// We use this to automatically name a file that will store
// the neovim connection's requests/responses so that we can
@@ -393,20 +393,20 @@ impl<'a> NeovimBackedTestContext<'a> {
pub fn binding<const COUNT: usize>(
self,
keystrokes: [&'static str; COUNT],
- ) -> NeovimBackedBindingTestContext<'a, COUNT> {
+ ) -> NeovimBackedBindingTestContext<COUNT> {
NeovimBackedBindingTestContext::new(keystrokes, self)
}
}
-impl<'a> Deref for NeovimBackedTestContext<'a> {
- type Target = VimTestContext<'a>;
+impl Deref for NeovimBackedTestContext {
+ type Target = VimTestContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
-impl<'a> DerefMut for NeovimBackedTestContext<'a> {
+impl DerefMut for NeovimBackedTestContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
@@ -415,7 +415,7 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
// a common mistake in tests is to call set_shared_state when
// you mean asswert_shared_state. This notices that and lets
// you know.
-impl<'a> Drop for NeovimBackedTestContext<'a> {
+impl Drop for NeovimBackedTestContext {
fn drop(&mut self) {
if self.is_dirty {
panic!("Test context was dropped after set_shared_state before assert_shared_state")
@@ -425,9 +425,8 @@ impl<'a> Drop for NeovimBackedTestContext<'a> {
#[cfg(test)]
mod test {
- use gpui::TestAppContext;
-
use crate::test::NeovimBackedTestContext;
+ use gpui::TestAppContext;
#[gpui::test]
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
@@ -10,11 +10,11 @@ use search::BufferSearchBar;
use crate::{state::Operator, *};
-pub struct VimTestContext<'a> {
- cx: EditorLspTestContext<'a>,
+pub struct VimTestContext {
+ cx: EditorLspTestContext,
}
-impl<'a> VimTestContext<'a> {
+impl VimTestContext {
pub fn init(cx: &mut gpui::TestAppContext) {
if cx.has_global::<Vim>() {
dbg!("OOPS");
@@ -29,13 +29,13 @@ impl<'a> VimTestContext<'a> {
});
}
- pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
+ pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext {
Self::init(cx);
let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
Self::new_with_lsp(lsp, enabled)
}
- pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
+ pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext {
Self::init(cx);
Self::new_with_lsp(
EditorLspTestContext::new_typescript(Default::default(), cx).await,
@@ -43,7 +43,7 @@ impl<'a> VimTestContext<'a> {
)
}
- pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
+ pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
@@ -162,15 +162,15 @@ impl<'a> VimTestContext<'a> {
}
}
-impl<'a> Deref for VimTestContext<'a> {
- type Target = EditorTestContext<'a>;
+impl Deref for VimTestContext {
+ type Target = EditorTestContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
-impl<'a> DerefMut for VimTestContext<'a> {
+impl DerefMut for VimTestContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
@@ -1,4 +1,5 @@
use super::base_keymap_setting::BaseKeymap;
+use client::telemetry::Telemetry;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
@@ -27,22 +28,22 @@ pub fn toggle(
cx: &mut ViewContext<Workspace>,
) {
let fs = workspace.app_state().fs.clone();
+ let telemetry = workspace.client().telemetry().clone();
workspace.toggle_modal(cx, |cx| {
BaseKeymapSelector::new(
- BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+ BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx),
cx,
)
});
}
pub struct BaseKeymapSelector {
- focus_handle: gpui::FocusHandle,
picker: View<Picker<BaseKeymapSelectorDelegate>>,
}
impl FocusableView for BaseKeymapSelector {
- fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
- self.focus_handle.clone()
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
}
}
@@ -55,17 +56,13 @@ impl BaseKeymapSelector {
cx: &mut ViewContext<BaseKeymapSelector>,
) -> Self {
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
- let focus_handle = cx.focus_handle();
- Self {
- focus_handle,
- picker,
- }
+ Self { picker }
}
}
impl Render for BaseKeymapSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- self.picker.clone()
+ v_stack().w(rems(34.)).child(self.picker.clone())
}
}
@@ -73,6 +70,7 @@ pub struct BaseKeymapSelectorDelegate {
view: WeakView<BaseKeymapSelector>,
matches: Vec<StringMatch>,
selected_index: usize,
+ telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
}
@@ -80,6 +78,7 @@ impl BaseKeymapSelectorDelegate {
fn new(
weak_view: WeakView<BaseKeymapSelector>,
fs: Arc<dyn Fs>,
+ telemetry: Arc<Telemetry>,
cx: &mut ViewContext<BaseKeymapSelector>,
) -> Self {
let base = BaseKeymap::get(None, cx);
@@ -91,6 +90,7 @@ impl BaseKeymapSelectorDelegate {
view: weak_view,
matches: Vec::new(),
selected_index,
+ telemetry,
fs,
}
}
@@ -172,6 +172,10 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
if let Some(selection) = self.matches.get(self.selected_index) {
let base_keymap = BaseKeymap::from_names(&selection.string);
+
+ self.telemetry
+ .report_setting_event("keymap", base_keymap.to_string());
+
update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
*setting = Some(base_keymap)
});
@@ -184,7 +188,13 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
.ok();
}
- fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
+ self.view
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .log_err();
+ }
fn render_match(
&self,
@@ -1,3 +1,5 @@
+use std::fmt::{Display, Formatter};
+
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -12,6 +14,18 @@ pub enum BaseKeymap {
TextMate,
}
+impl Display for BaseKeymap {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ BaseKeymap::VSCode => write!(f, "VSCode"),
+ BaseKeymap::JetBrains => write!(f, "JetBrains"),
+ BaseKeymap::SublimeText => write!(f, "Sublime Text"),
+ BaseKeymap::Atom => write!(f, "Atom"),
+ BaseKeymap::TextMate => write!(f, "TextMate"),
+ }
+ }
+}
+
impl BaseKeymap {
pub const OPTIONS: [(&'static str, Self); 5] = [
("VSCode (Default)", Self::VSCode),
@@ -1,7 +1,7 @@
mod base_keymap_picker;
mod base_keymap_setting;
-use client::TelemetrySettings;
+use client::{telemetry::Telemetry, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
@@ -27,7 +27,7 @@ pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &Welcome, cx| {
- let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+ let welcome_page = WelcomePage::new(workspace, cx);
workspace.add_item(Box::new(welcome_page), cx)
});
})
@@ -39,7 +39,7 @@ pub fn init(cx: &mut AppContext) {
pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
- let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+ let welcome_page = WelcomePage::new(workspace, cx);
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
cx.focus_view(&welcome_page);
cx.notify();
@@ -54,174 +54,213 @@ pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
pub struct WelcomePage {
workspace: WeakView<Workspace>,
focus_handle: FocusHandle,
+ telemetry: Arc<Telemetry>,
_settings_subscription: Subscription,
}
impl Render for WelcomePage {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
- h_stack()
- .full()
- .bg(cx.theme().colors().editor_background)
- .track_focus(&self.focus_handle)
- .child(
- v_stack()
- .w_96()
- .gap_4()
- .mx_auto()
- .child(
- svg()
- .path("icons/logo_96.svg")
- .text_color(gpui::white())
- .w(px(96.))
- .h(px(96.))
- .mx_auto(),
- )
- .child(
- h_stack()
- .justify_center()
- .child(Label::new("Code at the speed of thought")),
- )
- .child(
- v_stack()
- .gap_2()
- .child(
- Button::new("choose-theme", "Choose a theme")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- theme_selector::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("choose-keymap", "Choose a keymap")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- base_keymap_picker::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("install-cli", "Install the CLI")
- .full_width()
- .on_click(cx.listener(|_, _, cx| {
- cx.app_mut()
- .spawn(|cx| async move {
- install_cli::install_cli(&cx).await
- })
- .detach_and_log_err(cx);
- })),
- ),
- )
- .child(
- v_stack()
- .p_3()
- .gap_2()
- .bg(cx.theme().colors().elevated_surface_background)
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_md()
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-vim",
- if VimModeSetting::get_global(cx).0 {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
+ h_stack().full().track_focus(&self.focus_handle).child(
+ v_stack()
+ .w_96()
+ .gap_4()
+ .mx_auto()
+ .child(
+ svg()
+ .path("icons/logo_96.svg")
+ .text_color(gpui::white())
+ .w(px(96.))
+ .h(px(96.))
+ .mx_auto(),
+ )
+ .child(
+ h_stack()
+ .justify_center()
+ .child(Label::new("Code at the speed of thought")),
+ )
+ .child(
+ v_stack()
+ .gap_2()
+ .child(
+ Button::new("choose-theme", "Choose a theme")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry
+ .report_app_event("welcome page: change theme");
+ this.workspace
+ .update(cx, |workspace, cx| {
+ theme_selector::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
+ )
+ .child(
+ Button::new("choose-keymap", "Choose a keymap")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry
+ .report_app_event("welcome page: change keymap");
+ this.workspace
+ .update(cx, |workspace, cx| {
+ base_keymap_picker::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
+ )
+ .child(
+ Button::new("install-cli", "Install the CLI")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry.report_app_event("welcome page: install cli");
+ cx.app_mut()
+ .spawn(
+ |cx| async move { install_cli::install_cli(&cx).await },
)
- .on_click(
- cx.listener(move |this, selection, cx| {
- this.update_settings::<VimModeSetting>(
- selection,
- cx,
- |setting, value| *setting = Some(value),
- );
- }),
- ),
+ .detach_and_log_err(cx);
+ })),
+ ),
+ )
+ .child(
+ v_stack()
+ .p_3()
+ .gap_2()
+ .bg(cx.theme().colors().elevated_surface_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .rounded_md()
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-vim",
+ if VimModeSetting::get_global(cx).0 {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
)
- .child(Label::new("Enable vim mode")),
- )
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-telemetry",
- if TelemetrySettings::get_global(cx).metrics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(
- cx.listener(move |this, selection, cx| {
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- |settings, value| {
- settings.metrics = Some(value)
- },
- );
- }),
- ),
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.telemetry
+ .report_app_event("welcome page: toggle vim");
+ this.update_settings::<VimModeSetting>(
+ selection,
+ cx,
+ |setting, value| *setting = Some(value),
+ );
+ },
+ )),
+ )
+ .child(Label::new("Enable vim mode")),
+ )
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-telemetry",
+ if TelemetrySettings::get_global(cx).metrics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
)
- .child(Label::new("Send anonymous usage data")),
- )
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-crash",
- if TelemetrySettings::get_global(cx).diagnostics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(
- cx.listener(move |this, selection, cx| {
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- |settings, value| {
- settings.diagnostics = Some(value)
- },
- );
- }),
- ),
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: toggle metric telemetry",
+ );
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ {
+ let telemetry = this.telemetry.clone();
+
+ move |settings, value| {
+ settings.metrics = Some(value);
+
+ telemetry.report_setting_event(
+ "metric telemetry",
+ value.to_string(),
+ );
+ }
+ },
+ );
+ },
+ )),
+ )
+ .child(Label::new("Send anonymous usage data")),
+ )
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-crash",
+ if TelemetrySettings::get_global(cx).diagnostics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
)
- .child(Label::new("Send crash reports")),
- ),
- ),
- )
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: toggle diagnostic telemetry",
+ );
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ {
+ let telemetry = this.telemetry.clone();
+
+ move |settings, value| {
+ settings.diagnostics = Some(value);
+
+ telemetry.report_setting_event(
+ "diagnostic telemetry",
+ value.to_string(),
+ );
+ }
+ },
+ );
+ },
+ )),
+ )
+ .child(Label::new("Send crash reports")),
+ ),
+ ),
+ )
}
}
impl WelcomePage {
- pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
- WelcomePage {
- focus_handle: cx.focus_handle(),
- workspace: workspace.weak_handle(),
- _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
- }
+ pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+ let this = cx.new_view(|cx| {
+ cx.on_release(|this: &mut Self, _, _| {
+ this.telemetry.report_app_event("welcome page: close");
+ })
+ .detach();
+
+ WelcomePage {
+ focus_handle: cx.focus_handle(),
+ workspace: workspace.weak_handle(),
+ telemetry: workspace.client().telemetry().clone(),
+ _settings_subscription: cx
+ .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+ }
+ });
+
+ this
}
fn update_settings<T: Settings>(
@@ -279,6 +318,7 @@ impl Item for WelcomePage {
Some(cx.new_view(|cx| WelcomePage {
focus_handle: cx.focus_handle(),
workspace: self.workspace.clone(),
+ telemetry: self.telemetry.clone(),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
}))
}
@@ -19,7 +19,6 @@ pub enum PanelEvent {
ZoomOut,
Activate,
Close,
- Focus,
}
pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
@@ -216,6 +215,28 @@ impl Dock {
}
});
+ cx.on_focus_in(&focus_handle, {
+ let dock = dock.downgrade();
+ move |workspace, cx| {
+ let Some(dock) = dock.upgrade() else {
+ return;
+ };
+ let Some(panel) = dock.read(cx).active_panel() else {
+ return;
+ };
+ if panel.is_zoomed(cx) {
+ workspace.zoomed = Some(panel.to_any().downgrade().into());
+ workspace.zoomed_position = Some(position);
+ } else {
+ workspace.zoomed = None;
+ workspace.zoomed_position = None;
+ }
+ workspace.dismiss_zoomed_items_to_reveal(Some(position), cx);
+ workspace.update_active_view_for_followers(cx)
+ }
+ })
+ .detach();
+
cx.observe(&dock, move |workspace, dock, cx| {
if dock.read(cx).is_open() {
if let Some(panel) = dock.read(cx).active_panel() {
@@ -394,7 +415,6 @@ impl Dock {
this.set_open(false, cx);
}
}
- PanelEvent::Focus => {}
}),
];
@@ -561,6 +581,7 @@ impl Render for Dock {
}
div()
+ .track_focus(&self.focus_handle)
.flex()
.bg(cx.theme().colors().panel_background)
.border_color(cx.theme().colors().border)
@@ -584,7 +605,7 @@ impl Render for Dock {
)
.child(handle)
} else {
- div()
+ div().track_focus(&self.focus_handle)
}
}
}
@@ -724,7 +745,7 @@ pub mod test {
impl Render for TestPanel {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- div()
+ div().id("test").track_focus(&self.focus_handle)
}
}
@@ -442,7 +442,7 @@ impl<T: Item> ItemHandle for View<T> {
) && !pending_update_scheduled.load(Ordering::SeqCst)
{
pending_update_scheduled.store(true, Ordering::SeqCst);
- cx.on_next_frame({
+ cx.defer({
let pending_update = pending_update.clone();
let pending_update_scheduled = pending_update_scheduled.clone();
move |this, cx| {
@@ -2,15 +2,16 @@ use crate::{
item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
- NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
+ NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
- AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
- FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
- ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+ AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,
+ FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point,
+ PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext,
+ WeakView, WindowContext,
};
use parking_lot::Mutex;
use project::{Project, ProjectEntryId, ProjectPath};
@@ -19,6 +20,7 @@ use settings::Settings;
use std::{
any::Any,
cmp, fmt, mem,
+ ops::ControlFlow,
path::{Path, PathBuf},
rc::Rc,
sync::{
@@ -182,6 +184,8 @@ pub struct Pane {
project: Model<Project>,
drag_split_direction: Option<SplitDirection>,
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
+ custom_drop_handle:
+ Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
can_split: bool,
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
_subscriptions: Vec<Subscription>,
@@ -374,6 +378,7 @@ impl Pane {
workspace,
project,
can_drop_predicate,
+ custom_drop_handle: None,
can_split: true,
render_tab_bar_buttons: Rc::new(move |pane, cx| {
h_stack()
@@ -500,13 +505,6 @@ impl Pane {
self.active_item_index
}
- // pub fn on_can_drop<F>(&mut self, can_drop: F)
- // where
- // F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
- // {
- // self.can_drop = Rc::new(can_drop);
- // }
-
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
self.can_split = can_split;
cx.notify();
@@ -527,6 +525,14 @@ impl Pane {
cx.notify();
}
+ pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
+ where
+ F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
+ {
+ self.custom_drop_handle = Some(Arc::new(handle));
+ cx.notify();
+ }
+
pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
ItemNavHistory {
history: self.nav_history.clone(),
@@ -1555,6 +1561,10 @@ impl Pane {
this.drag_split_direction = None;
this.handle_project_entry_drop(entry_id, cx)
}))
+ .on_drop(cx.listener(move |this, paths, cx| {
+ this.drag_split_direction = None;
+ this.handle_external_paths_drop(paths, cx)
+ }))
.when_some(item.tab_tooltip_text(cx), |tab, text| {
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
})
@@ -1721,6 +1731,10 @@ impl Pane {
.on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
this.drag_split_direction = None;
this.handle_project_entry_drop(entry_id, cx)
+ }))
+ .on_drop(cx.listener(move |this, paths, cx| {
+ this.drag_split_direction = None;
+ this.handle_external_paths_drop(paths, cx)
})),
)
}
@@ -1809,8 +1823,13 @@ impl Pane {
&mut self,
dragged_tab: &DraggedTab,
ix: usize,
- cx: &mut ViewContext<'_, Pane>,
+ cx: &mut ViewContext<'_, Self>,
) {
+ if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+ if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
+ return;
+ }
+ }
let mut to_pane = cx.view().clone();
let split_direction = self.drag_split_direction;
let item_id = dragged_tab.item_id;
@@ -1830,8 +1849,13 @@ impl Pane {
fn handle_project_entry_drop(
&mut self,
project_entry_id: &ProjectEntryId,
- cx: &mut ViewContext<'_, Pane>,
+ cx: &mut ViewContext<'_, Self>,
) {
+ if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+ if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
+ return;
+ }
+ }
let mut to_pane = cx.view().clone();
let split_direction = self.drag_split_direction;
let project_entry_id = *project_entry_id;
@@ -1855,6 +1879,38 @@ impl Pane {
.log_err();
}
+ fn handle_external_paths_drop(
+ &mut self,
+ paths: &ExternalPaths,
+ cx: &mut ViewContext<'_, Self>,
+ ) {
+ if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+ if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
+ return;
+ }
+ }
+ let mut to_pane = cx.view().clone();
+ let 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);
+ }
+ workspace
+ .open_paths(
+ paths,
+ OpenVisible::OnlyDirectories,
+ Some(to_pane.downgrade()),
+ cx,
+ )
+ .detach();
+ });
+ })
+ .log_err();
+ }
+
pub fn display_nav_history_buttons(&mut self, display: bool) {
self.display_nav_history_buttons = display;
}
@@ -1956,6 +2012,7 @@ impl Render for Pane {
.group("")
.on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
.on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
+ .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
.map(|div| {
if let Some(item) = self.active_item() {
div.v_flex()
@@ -1985,6 +2042,7 @@ impl Render for Pane {
))
.group_drag_over::<DraggedTab>("", |style| style.visible())
.group_drag_over::<ProjectEntryId>("", |style| style.visible())
+ .group_drag_over::<ExternalPaths>("", |style| style.visible())
.when_some(self.can_drop_predicate.clone(), |this, p| {
this.can_drop(move |a, cx| p(a, cx))
})
@@ -1994,6 +2052,9 @@ impl Render for Pane {
.on_drop(cx.listener(move |this, entry_id, cx| {
this.handle_project_entry_drop(entry_id, cx)
}))
+ .on_drop(cx.listener(move |this, paths, cx| {
+ this.handle_external_paths_drop(paths, cx)
+ }))
.map(|div| match self.drag_split_direction {
None => div.top_0().left_0().right_0().bottom_0(),
Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
@@ -487,6 +487,7 @@ impl PaneAxis {
basis,
self.flexes.clone(),
self.bounding_boxes.clone(),
+ cx.view().downgrade(),
)
.children(self.members.iter().enumerate().map(|(ix, member)| {
if member.contains(active_pane) {
@@ -575,21 +576,25 @@ mod element {
use gpui::{
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
- Size, Style, WindowContext,
+ Size, Style, WeakView, WindowContext,
};
use parking_lot::Mutex;
use smallvec::SmallVec;
use ui::prelude::*;
+ use util::ResultExt;
+
+ use crate::Workspace;
use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
const DIVIDER_SIZE: f32 = 1.0;
- pub fn pane_axis(
+ pub(super) fn pane_axis(
axis: Axis,
basis: usize,
flexes: Arc<Mutex<Vec<f32>>>,
bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+ workspace: WeakView<Workspace>,
) -> PaneAxisElement {
PaneAxisElement {
axis,
@@ -598,6 +603,7 @@ mod element {
bounding_boxes,
children: SmallVec::new(),
active_pane_ix: None,
+ workspace,
}
}
@@ -608,6 +614,7 @@ mod element {
bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
children: SmallVec<[AnyElement; 2]>,
active_pane_ix: Option<usize>,
+ workspace: WeakView<Workspace>,
}
impl PaneAxisElement {
@@ -623,6 +630,7 @@ mod element {
axis: Axis,
child_start: Point<Pixels>,
container_size: Size<Pixels>,
+ workspace: WeakView<Workspace>,
cx: &mut WindowContext,
) {
let min_size = match axis {
@@ -697,7 +705,9 @@ mod element {
}
// todo!(schedule serialize)
- // workspace.schedule_serialize(cx);
+ workspace
+ .update(cx, |this, cx| this.schedule_serialize(cx))
+ .log_err();
cx.notify();
}
@@ -708,6 +718,7 @@ mod element {
ix: usize,
pane_bounds: Bounds<Pixels>,
axis_bounds: Bounds<Pixels>,
+ workspace: WeakView<Workspace>,
cx: &mut WindowContext,
) {
let handle_bounds = Bounds {
@@ -742,24 +753,39 @@ mod element {
cx.on_mouse_event({
let dragged_handle = dragged_handle.clone();
- move |e: &MouseDownEvent, phase, _cx| {
+ let flexes = flexes.clone();
+ let workspace = workspace.clone();
+ move |e: &MouseDownEvent, phase, cx| {
if phase.bubble() && handle_bounds.contains(&e.position) {
dragged_handle.replace(Some(ix));
+ if e.click_count >= 2 {
+ let mut borrow = flexes.lock();
+ *borrow = vec![1.; borrow.len()];
+ workspace
+ .update(cx, |this, cx| this.schedule_serialize(cx))
+ .log_err();
+ cx.notify();
+ }
}
}
});
- cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
- let dragged_handle = dragged_handle.borrow();
- if phase.bubble() && *dragged_handle == Some(ix) {
- Self::compute_resize(
- &flexes,
- e,
- ix,
- axis,
- pane_bounds.origin,
- axis_bounds.size,
- cx,
- )
+ cx.on_mouse_event({
+ let workspace = workspace.clone();
+ move |e: &MouseMoveEvent, phase, cx| {
+ let dragged_handle = dragged_handle.borrow();
+
+ if phase.bubble() && *dragged_handle == Some(ix) {
+ Self::compute_resize(
+ &flexes,
+ e,
+ ix,
+ axis,
+ pane_bounds.origin,
+ axis_bounds.size,
+ workspace.clone(),
+ cx,
+ )
+ }
}
});
});
@@ -840,6 +866,7 @@ mod element {
ix,
child_bounds,
bounds,
+ self.workspace.clone(),
cx,
);
}
@@ -66,12 +66,16 @@ impl FocusableView for SharedScreen {
}
}
impl Render for SharedScreen {
- fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
- div().track_focus(&self.focus).size_full().children(
- self.frame
- .as_ref()
- .map(|frame| img(frame.image()).size_full()),
- )
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ div()
+ .bg(cx.theme().colors().editor_background)
+ .track_focus(&self.focus)
+ .size_full()
+ .children(
+ self.frame
+ .as_ref()
+ .map(|frame| img(frame.image()).size_full()),
+ )
}
}
@@ -431,6 +431,13 @@ pub enum Event {
WorkspaceCreated(WeakView<Workspace>),
}
+pub enum OpenVisible {
+ All,
+ None,
+ OnlyFiles,
+ OnlyDirectories,
+}
+
pub struct Workspace {
weak_self: WeakView<Self>,
workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
@@ -651,7 +658,7 @@ impl Workspace {
cx.on_release(|this, window, cx| {
this.app_state.workspace_store.update(cx, |store, _| {
let window = window.downcast::<Self>().unwrap();
- debug_assert!(store.workspaces.remove(&window));
+ store.workspaces.remove(&window);
})
}),
];
@@ -1258,9 +1265,7 @@ impl Workspace {
}
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
- self.client()
- .telemetry()
- .report_app_event("open project", false, cx);
+ self.client().telemetry().report_app_event("open project");
let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
@@ -1317,7 +1322,8 @@ impl Workspace {
pub fn open_paths(
&mut self,
mut abs_paths: Vec<PathBuf>,
- visible: bool,
+ visible: OpenVisible,
+ pane: Option<WeakView<Pane>>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
log::info!("open paths {abs_paths:?}");
@@ -1328,31 +1334,56 @@ impl Workspace {
abs_paths.sort_unstable();
cx.spawn(move |this, mut cx| async move {
let mut tasks = Vec::with_capacity(abs_paths.len());
+
for abs_path in &abs_paths {
- let project_path = match this
- .update(&mut cx, |this, cx| {
- Workspace::project_path_for_path(
- this.project.clone(),
- abs_path,
- visible,
- cx,
- )
- })
- .log_err()
- {
- Some(project_path) => project_path.await.log_err(),
+ let visible = match visible {
+ OpenVisible::All => Some(true),
+ OpenVisible::None => Some(false),
+ OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
+ Some(Some(metadata)) => Some(!metadata.is_dir),
+ Some(None) => {
+ log::error!("No metadata for file {abs_path:?}");
+ None
+ }
+ None => None,
+ },
+ OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
+ Some(Some(metadata)) => Some(metadata.is_dir),
+ Some(None) => {
+ log::error!("No metadata for file {abs_path:?}");
+ None
+ }
+ None => None,
+ },
+ };
+ let project_path = match visible {
+ Some(visible) => match this
+ .update(&mut cx, |this, cx| {
+ Workspace::project_path_for_path(
+ this.project.clone(),
+ abs_path,
+ visible,
+ cx,
+ )
+ })
+ .log_err()
+ {
+ Some(project_path) => project_path.await.log_err(),
+ None => None,
+ },
None => None,
};
let this = this.clone();
let abs_path = abs_path.clone();
let fs = fs.clone();
+ let pane = pane.clone();
let task = cx.spawn(move |mut cx| async move {
let (worktree, project_path) = project_path?;
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
- this.open_path(project_path, None, true, cx)
+ this.open_path(project_path, pane, true, cx)
})
.log_err()?
.await,
@@ -1398,7 +1429,9 @@ impl Workspace {
cx.spawn(|this, mut cx| async move {
if let Some(paths) = paths.await.log_err().flatten() {
let results = this
- .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+ .update(&mut cx, |this, cx| {
+ this.open_paths(paths, OpenVisible::All, None, cx)
+ })?
.await;
for result in results.into_iter().flatten() {
result.log_err();
@@ -1784,7 +1817,16 @@ impl Workspace {
cx.spawn(|workspace, mut cx| async move {
let open_paths_task_result = workspace
.update(&mut cx, |workspace, cx| {
- workspace.open_paths(vec![abs_path.clone()], visible, cx)
+ workspace.open_paths(
+ vec![abs_path.clone()],
+ if visible {
+ OpenVisible::All
+ } else {
+ OpenVisible::None
+ },
+ None,
+ cx,
+ )
})
.with_context(|| format!("open abs path {abs_path:?} task spawn"))?
.await;
@@ -2457,11 +2499,11 @@ impl Workspace {
Some(leader_id)
}
- // pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
- // self.follower_states
- // .values()
- // .any(|state| state.leader_id == peer_id)
- // }
+ pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
+ self.follower_states
+ .values()
+ .any(|state| state.leader_id == peer_id)
+ }
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
@@ -4083,7 +4125,7 @@ pub fn open_paths(
existing.clone(),
existing
.update(&mut cx, |workspace, cx| {
- workspace.open_paths(abs_paths, true, cx)
+ workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
})?
.await,
))
@@ -4131,7 +4173,7 @@ pub fn create_and_open_local_file(
let mut items = workspace
.update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(cx, |workspace, cx| {
- workspace.open_paths(vec![path.to_path_buf()], false, cx)
+ workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
})
})?
.await?
@@ -146,8 +146,7 @@ uuid.workspace = true
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
# client = { path = "../client", features = ["test-support"] }
-# editor = { path = "../editor", features = ["test-support"] }
-# gpui = { path = "../gpui", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
# lsp = { path = "../lsp", features = ["test-support"] }
@@ -156,7 +155,7 @@ project = { path = "../project", features = ["test-support"] }
# settings = { path = "../settings", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
# util = { path = "../util", features = ["test-support"] }
-# workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
unindent.workspace = true
[package.metadata.bundle-dev]
@@ -45,7 +45,7 @@ use util::{
paths, ResultExt,
};
use uuid::Uuid;
-use welcome::{show_welcome_view, FIRST_OPEN};
+use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
use workspace::{AppState, WorkspaceStore};
use zed::{
app_menus, build_window_options, ensure_only_instance, handle_cli_connection,
@@ -171,17 +171,15 @@ fn main() {
})
.detach();
- client.telemetry().start(installation_id, session_id, cx);
- client
- .telemetry()
- .report_setting_event("theme", cx.theme().name.to_string(), cx);
- let event_operation = match existing_installation_id_found {
+ let telemetry = client.telemetry();
+ telemetry.start(installation_id, session_id, cx);
+ telemetry.report_setting_event("theme", cx.theme().name.to_string());
+ telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
+ telemetry.report_app_event(match existing_installation_id_found {
Some(false) => "first open",
_ => "open",
- };
- client
- .telemetry()
- .report_app_event(event_operation, true, cx);
+ });
+ telemetry.flush_events();
let app_state = Arc::new(AppState {
languages: languages.clone(),
@@ -33,11 +33,11 @@ use util::{
};
use uuid::Uuid;
use welcome::BaseKeymap;
-use workspace::Pane;
use workspace::{
create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings,
};
+use workspace::{dock::Panel, Pane};
use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit};
actions!(
@@ -114,9 +114,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
})
.detach();
- // cx.emit(workspace2::Event::PaneAdded(
- // workspace.active_pane().clone(),
- // ));
+ // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
// let collab_titlebar_item =
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
@@ -187,6 +185,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
+ let position = project_panel.read(cx).position(cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(terminal_panel, cx);
workspace.add_panel(assistant_panel, cx);
@@ -194,19 +193,18 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.add_panel(chat_panel, cx);
workspace.add_panel(notification_panel, cx);
- // if !was_deserialized
- // && workspace
- // .project()
- // .read(cx)
- // .visible_worktrees(cx)
- // .any(|tree| {
- // tree.read(cx)
- // .root_entry()
- // .map_or(false, |entry| entry.is_dir())
- // })
- // {
- // workspace.toggle_dock(project_panel_position, cx);
- // }
+ if workspace
+ .project()
+ .read(cx)
+ .visible_worktrees(cx)
+ .any(|tree| {
+ tree.read(cx)
+ .root_entry()
+ .map_or(false, |entry| entry.is_dir())
+ })
+ {
+ workspace.toggle_dock(position, cx);
+ }
cx.focus_self();
})
})
@@ -587,7 +585,6 @@ pub fn handle_keymap_file_changes(
}
}
}
-
cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
}
})
@@ -770,1844 +767,2073 @@ fn open_bundled_file(
}
// todo!()
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use assets::Assets;
-// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
-// use fs::{FakeFs, Fs};
-// use gpui::{
-// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
-// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
-// };
-// use language::LanguageRegistry;
-// use project::{project_settings::ProjectSettings, Project, ProjectPath};
-// use serde_json::json;
-// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
-// use std::{
-// collections::HashSet,
-// path::{Path, PathBuf},
-// };
-// use theme::{ThemeRegistry, ThemeSettings};
-// use workspace::{
-// item::{Item, ItemHandle},
-// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
-// };
-
-// #[gpui::test]
-// async fn test_open_paths_action(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {
-// "aa": null,
-// "ab": null,
-// },
-// "b": {
-// "ba": null,
-// "bb": null,
-// },
-// "c": {
-// "ca": null,
-// "cb": null,
-// },
-// "d": {
-// "da": null,
-// "db": null,
-// },
-// }),
-// )
-// .await;
-
-// cx.update(|cx| {
-// open_paths(
-// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
-// &app_state,
-// None,
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 1);
-
-// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 1);
-// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-// workspace_1.update(cx, |workspace, cx| {
-// assert_eq!(workspace.worktrees(cx).count(), 2);
-// assert!(workspace.left_dock().read(cx).is_open());
-// assert!(workspace.active_pane().is_focused(cx));
-// });
-
-// cx.update(|cx| {
-// open_paths(
-// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
-// &app_state,
-// None,
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 2);
-
-// // Replace existing windows
-// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
-// cx.update(|cx| {
-// open_paths(
-// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
-// &app_state,
-// Some(window),
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 2);
-// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-// workspace_1.update(cx, |workspace, cx| {
-// assert_eq!(
-// workspace
-// .worktrees(cx)
-// .map(|w| w.read(cx).abs_path())
-// .collect::<Vec<_>>(),
-// &[Path::new("/root/c").into(), Path::new("/root/d").into()]
-// );
-// assert!(workspace.left_dock().read(cx).is_open());
-// assert!(workspace.active_pane().is_focused(cx));
-// });
-// }
-
-// #[gpui::test]
-// async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree("/root", json!({"a": "hey"}))
-// .await;
-
-// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 1);
-
-// // When opening the workspace, the window is not in a edited state.
-// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
-// let workspace = window.root(cx);
-// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-// let editor = workspace.read_with(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// assert!(!window.is_edited(cx));
-
-// // Editing a buffer marks the window as edited.
-// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-// assert!(window.is_edited(cx));
-
-// // Undoing the edit restores the window's edited state.
-// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
-// assert!(!window.is_edited(cx));
-
-// // Redoing the edit marks the window as edited again.
-// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
-// assert!(window.is_edited(cx));
-
-// // Closing the item restores the window's edited state.
-// let close = pane.update(cx, |pane, cx| {
-// drop(editor);
-// pane.close_active_item(&Default::default(), cx).unwrap()
-// });
-// executor.run_until_parked();
-
-// window.simulate_prompt_answer(1, cx);
-// close.await.unwrap();
-// assert!(!window.is_edited(cx));
-
-// // Opening the buffer again doesn't impact the window's edited state.
-// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-// .await
-// .unwrap();
-// let editor = workspace.read_with(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// assert!(!window.is_edited(cx));
-
-// // Editing the buffer marks the window as edited.
-// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-// assert!(window.is_edited(cx));
-
-// // Ensure closing the window via the mouse gets preempted due to the
-// // buffer having unsaved changes.
-// assert!(!window.simulate_close(cx));
-// executor.run_until_parked();
-// assert_eq!(cx.windows().len(), 1);
-
-// // The window is successfully closed after the user dismisses the prompt.
-// window.simulate_prompt_answer(1, cx);
-// executor.run_until_parked();
-// assert_eq!(cx.windows().len(), 0);
-// }
-
-// #[gpui::test]
-// async fn test_new_empty_workspace(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// cx.update(|cx| {
-// open_new(&app_state, cx, |workspace, cx| {
-// Editor::new_file(workspace, &Default::default(), cx)
-// })
-// })
-// .await;
-
-// let window = cx
-// .windows()
-// .first()
-// .unwrap()
-// .downcast::<Workspace>()
-// .unwrap();
-// let workspace = window.root(cx);
-
-// let editor = workspace.update(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<editor::Editor>()
-// .unwrap()
-// });
-
-// editor.update(cx, |editor, cx| {
-// assert!(editor.text(cx).is_empty());
-// assert!(!editor.is_dirty(cx));
-// });
-
-// let save_task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(SaveIntent::Save, cx)
-// });
-// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-// cx.foreground().run_until_parked();
-// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
-// save_task.await.unwrap();
-// editor.read_with(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert_eq!(editor.title(cx), "the-new-name");
-// });
-// }
-
-// #[gpui::test]
-// async fn test_open_entry(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {
-// "file1": "contents 1",
-// "file2": "contents 2",
-// "file3": "contents 3",
-// },
-// }),
-// )
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-
-// let entries = cx.read(|cx| workspace.file_project_paths(cx));
-// let file1 = entries[0].clone();
-// let file2 = entries[1].clone();
-// let file3 = entries[2].clone();
-
-// // Open the first entry
-// let entry_1 = workspace
-// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-// .await
-// .unwrap();
-// cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// assert_eq!(
-// pane.active_item().unwrap().project_path(cx),
-// Some(file1.clone())
-// );
-// assert_eq!(pane.items_len(), 1);
-// });
-
-// // Open the second entry
-// workspace
-// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-// .await
-// .unwrap();
-// cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// assert_eq!(
-// pane.active_item().unwrap().project_path(cx),
-// Some(file2.clone())
-// );
-// assert_eq!(pane.items_len(), 2);
-// });
-
-// // Open the first entry again. The existing pane item is activated.
-// let entry_1b = workspace
-// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-// .await
-// .unwrap();
-// assert_eq!(entry_1.id(), entry_1b.id());
-
-// cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// assert_eq!(
-// pane.active_item().unwrap().project_path(cx),
-// Some(file1.clone())
-// );
-// assert_eq!(pane.items_len(), 2);
-// });
-
-// // Split the pane with the first entry, then open the second entry again.
-// workspace
-// .update(cx, |w, cx| {
-// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
-// w.open_path(file2.clone(), None, true, cx)
-// })
-// .await
-// .unwrap();
-
-// workspace.read_with(cx, |w, cx| {
-// assert_eq!(
-// w.active_pane()
-// .read(cx)
-// .active_item()
-// .unwrap()
-// .project_path(cx),
-// Some(file2.clone())
-// );
-// });
-
-// // Open the third entry twice concurrently. Only one pane item is added.
-// let (t1, t2) = workspace.update(cx, |w, cx| {
-// (
-// w.open_path(file3.clone(), None, true, cx),
-// w.open_path(file3.clone(), None, true, cx),
-// )
-// });
-// t1.await.unwrap();
-// t2.await.unwrap();
-// cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// assert_eq!(
-// pane.active_item().unwrap().project_path(cx),
-// Some(file3.clone())
-// );
-// let pane_entries = pane
-// .items()
-// .map(|i| i.project_path(cx).unwrap())
-// .collect::<Vec<_>>();
-// assert_eq!(pane_entries, &[file1, file2, file3]);
-// });
-// }
-
-// #[gpui::test]
-// async fn test_open_paths(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/",
-// json!({
-// "dir1": {
-// "a.txt": ""
-// },
-// "dir2": {
-// "b.txt": ""
-// },
-// "dir3": {
-// "c.txt": ""
-// },
-// "d.txt": ""
-// }),
-// )
-// .await;
-
-// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
-// .await
-// .unwrap();
-// assert_eq!(cx.windows().len(), 1);
-// let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-
-// #[track_caller]
-// fn assert_project_panel_selection(
-// workspace: &Workspace,
-// expected_worktree_path: &Path,
-// expected_entry_path: &Path,
-// cx: &AppContext,
-// ) {
-// let project_panel = [
-// workspace.left_dock().read(cx).panel::<ProjectPanel>(),
-// workspace.right_dock().read(cx).panel::<ProjectPanel>(),
-// workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
-// ]
-// .into_iter()
-// .find_map(std::convert::identity)
-// .expect("found no project panels")
-// .read(cx);
-// let (selected_worktree, selected_entry) = project_panel
-// .selected_entry(cx)
-// .expect("project panel should have a selected entry");
-// assert_eq!(
-// selected_worktree.abs_path().as_ref(),
-// expected_worktree_path,
-// "Unexpected project panel selected worktree path"
-// );
-// assert_eq!(
-// selected_entry.path.as_ref(),
-// expected_entry_path,
-// "Unexpected project panel selected entry path"
-// );
-// }
-
-// // Open a file within an existing worktree.
-// workspace
-// .update(cx, |view, cx| {
-// view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
-// })
-// .await;
-// cx.read(|cx| {
-// let workspace = workspace.read(cx);
-// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
-// assert_eq!(
-// workspace
-// .active_pane()
-// .read(cx)
-// .active_item()
-// .unwrap()
-// .as_any()
-// .downcast_ref::<Editor>()
-// .unwrap()
-// .read(cx)
-// .title(cx),
-// "a.txt"
-// );
-// });
-
-// // Open a file outside of any existing worktree.
-// workspace
-// .update(cx, |view, cx| {
-// view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
-// })
-// .await;
-// cx.read(|cx| {
-// let workspace = workspace.read(cx);
-// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
-// let worktree_roots = workspace
-// .worktrees(cx)
-// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-// .collect::<HashSet<_>>();
-// assert_eq!(
-// worktree_roots,
-// vec!["/dir1", "/dir2/b.txt"]
-// .into_iter()
-// .map(Path::new)
-// .collect(),
-// );
-// assert_eq!(
-// workspace
-// .active_pane()
-// .read(cx)
-// .active_item()
-// .unwrap()
-// .as_any()
-// .downcast_ref::<Editor>()
-// .unwrap()
-// .read(cx)
-// .title(cx),
-// "b.txt"
-// );
-// });
-
-// // Ensure opening a directory and one of its children only adds one worktree.
-// workspace
-// .update(cx, |view, cx| {
-// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
-// })
-// .await;
-// cx.read(|cx| {
-// let workspace = workspace.read(cx);
-// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
-// let worktree_roots = workspace
-// .worktrees(cx)
-// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-// .collect::<HashSet<_>>();
-// assert_eq!(
-// worktree_roots,
-// vec!["/dir1", "/dir2/b.txt", "/dir3"]
-// .into_iter()
-// .map(Path::new)
-// .collect(),
-// );
-// assert_eq!(
-// workspace
-// .active_pane()
-// .read(cx)
-// .active_item()
-// .unwrap()
-// .as_any()
-// .downcast_ref::<Editor>()
-// .unwrap()
-// .read(cx)
-// .title(cx),
-// "c.txt"
-// );
-// });
-
-// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
-// workspace
-// .update(cx, |view, cx| {
-// view.open_paths(vec!["/d.txt".into()], false, cx)
-// })
-// .await;
-// cx.read(|cx| {
-// let workspace = workspace.read(cx);
-// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
-// let worktree_roots = workspace
-// .worktrees(cx)
-// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-// .collect::<HashSet<_>>();
-// assert_eq!(
-// worktree_roots,
-// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
-// .into_iter()
-// .map(Path::new)
-// .collect(),
-// );
-
-// let visible_worktree_roots = workspace
-// .visible_worktrees(cx)
-// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-// .collect::<HashSet<_>>();
-// assert_eq!(
-// visible_worktree_roots,
-// vec!["/dir1", "/dir2/b.txt", "/dir3"]
-// .into_iter()
-// .map(Path::new)
-// .collect(),
-// );
-
-// assert_eq!(
-// workspace
-// .active_pane()
-// .read(cx)
-// .active_item()
-// .unwrap()
-// .as_any()
-// .downcast_ref::<Editor>()
-// .unwrap()
-// .read(cx)
-// .title(cx),
-// "d.txt"
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// cx.update(|cx| {
-// cx.update_global::<SettingsStore, _, _>(|store, cx| {
-// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-// project_settings.file_scan_exclusions =
-// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
-// });
-// });
-// });
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// ".gitignore": "ignored_dir\n",
-// ".git": {
-// "HEAD": "ref: refs/heads/main",
-// },
-// "regular_dir": {
-// "file": "regular file contents",
-// },
-// "ignored_dir": {
-// "ignored_subdir": {
-// "file": "ignored subfile contents",
-// },
-// "file": "ignored file contents",
-// },
-// "excluded_dir": {
-// "file": "excluded file contents",
-// },
-// }),
-// )
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-
-// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
-// let paths_to_open = [
-// Path::new("/root/excluded_dir/file").to_path_buf(),
-// Path::new("/root/.git/HEAD").to_path_buf(),
-// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
-// ];
-// let (opened_workspace, new_items) = cx
-// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
-// .await
-// .unwrap();
-
-// assert_eq!(
-// opened_workspace.id(),
-// workspace.id(),
-// "Excluded files in subfolders of a workspace root should be opened in the workspace"
-// );
-// let mut opened_paths = cx.read(|cx| {
-// assert_eq!(
-// new_items.len(),
-// paths_to_open.len(),
-// "Expect to get the same number of opened items as submitted paths to open"
-// );
-// new_items
-// .iter()
-// .zip(paths_to_open.iter())
-// .map(|(i, path)| {
-// match i {
-// Some(Ok(i)) => {
-// Some(i.project_path(cx).map(|p| p.path.display().to_string()))
-// }
-// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
-// None => None,
-// }
-// .flatten()
-// })
-// .collect::<Vec<_>>()
-// });
-// opened_paths.sort();
-// assert_eq!(
-// opened_paths,
-// vec![
-// None,
-// Some(".git/HEAD".to_string()),
-// Some("excluded_dir/file".to_string()),
-// ],
-// "Excluded files should get opened, excluded dir should not get opened"
-// );
-
-// let entries = cx.read(|cx| workspace.file_project_paths(cx));
-// assert_eq!(
-// initial_entries, entries,
-// "Workspace entries should not change after opening excluded files and directories paths"
-// );
-
-// cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// let mut opened_buffer_paths = pane
-// .items()
-// .map(|i| {
-// i.project_path(cx)
-// .expect("all excluded files that got open should have a path")
-// .path
-// .display()
-// .to_string()
-// })
-// .collect::<Vec<_>>();
-// opened_buffer_paths.sort();
-// assert_eq!(
-// opened_buffer_paths,
-// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
-// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_save_conflicting_item(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree("/root", json!({ "a.txt": "" }))
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-
-// // Open a file within an existing worktree.
-// workspace
-// .update(cx, |view, cx| {
-// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
-// })
-// .await;
-// let editor = cx.read(|cx| {
-// let pane = workspace.read(cx).active_pane().read(cx);
-// let item = pane.active_item().unwrap();
-// item.downcast::<Editor>().unwrap()
-// });
-
-// editor.update(cx, |editor, cx| editor.handle_input("x", cx));
-// app_state
-// .fs
-// .as_fake()
-// .insert_file("/root/a.txt", "changed".to_string())
-// .await;
-// editor
-// .condition(cx, |editor, cx| editor.has_conflict(cx))
-// .await;
-// cx.read(|cx| assert!(editor.is_dirty(cx)));
-
-// let save_task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(SaveIntent::Save, cx)
-// });
-// cx.foreground().run_until_parked();
-// window.simulate_prompt_answer(0, cx);
-// save_task.await.unwrap();
-// editor.read_with(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert!(!editor.has_conflict(cx));
-// });
-// }
-
-// #[gpui::test]
-// async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// project.update(cx, |project, _| project.languages().add(rust_lang()));
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
-
-// // Create a new untitled buffer
-// cx.dispatch_action(window.into(), NewFile);
-// let editor = workspace.read_with(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-
-// editor.update(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert_eq!(editor.title(cx), "untitled");
-// assert!(Arc::ptr_eq(
-// &editor.language_at(0, cx).unwrap(),
-// &languages::PLAIN_TEXT
-// ));
-// editor.handle_input("hi", cx);
-// assert!(editor.is_dirty(cx));
-// });
-
-// // Save the buffer. This prompts for a filename.
-// let save_task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(SaveIntent::Save, cx)
-// });
-// cx.foreground().run_until_parked();
-// cx.simulate_new_path_selection(|parent_dir| {
-// assert_eq!(parent_dir, Path::new("/root"));
-// Some(parent_dir.join("the-new-name.rs"))
-// });
-// cx.read(|cx| {
-// assert!(editor.is_dirty(cx));
-// assert_eq!(editor.read(cx).title(cx), "untitled");
-// });
-
-// // When the save completes, the buffer's title is updated and the language is assigned based
-// // on the path.
-// save_task.await.unwrap();
-// editor.read_with(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert_eq!(editor.title(cx), "the-new-name.rs");
-// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
-// });
-
-// // Edit the file and save it again. This time, there is no filename prompt.
-// editor.update(cx, |editor, cx| {
-// editor.handle_input(" there", cx);
-// assert!(editor.is_dirty(cx));
-// });
-// let save_task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(SaveIntent::Save, cx)
-// });
-// save_task.await.unwrap();
-// assert!(!cx.did_prompt_for_new_path());
-// editor.read_with(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert_eq!(editor.title(cx), "the-new-name.rs")
-// });
-
-// // Open the same newly-created file in another pane item. The new editor should reuse
-// // the same buffer.
-// cx.dispatch_action(window.into(), NewFile);
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.split_and_clone(
-// workspace.active_pane().clone(),
-// SplitDirection::Right,
-// cx,
-// );
-// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
-// })
-// .await
-// .unwrap();
-// let editor2 = workspace.update(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-// cx.read(|cx| {
-// assert_eq!(
-// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
-// editor.read(cx).buffer().read(cx).as_singleton().unwrap()
-// );
-// })
-// }
-
-// #[gpui::test]
-// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-
-// let project = Project::test(app_state.fs.clone(), [], cx).await;
-// project.update(cx, |project, _| project.languages().add(rust_lang()));
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-
-// // Create a new untitled buffer
-// cx.dispatch_action(window.into(), NewFile);
-// let editor = workspace.read_with(cx, |workspace, cx| {
-// workspace
-// .active_item(cx)
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap()
-// });
-
-// editor.update(cx, |editor, cx| {
-// assert!(Arc::ptr_eq(
-// &editor.language_at(0, cx).unwrap(),
-// &languages::PLAIN_TEXT
-// ));
-// editor.handle_input("hi", cx);
-// assert!(editor.is_dirty(cx));
-// });
-
-// // Save the buffer. This prompts for a filename.
-// let save_task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(SaveIntent::Save, cx)
-// });
-// cx.foreground().run_until_parked();
-// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
-// save_task.await.unwrap();
-// // The buffer is not dirty anymore and the language is assigned based on the path.
-// editor.read_with(cx, |editor, cx| {
-// assert!(!editor.is_dirty(cx));
-// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
-// });
-// }
-
-// #[gpui::test]
-// async fn test_pane_actions(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {
-// "file1": "contents 1",
-// "file2": "contents 2",
-// "file3": "contents 3",
-// },
-// }),
-// )
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let workspace = window.root(cx);
-
-// let entries = cx.read(|cx| workspace.file_project_paths(cx));
-// let file1 = entries[0].clone();
-
-// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
-
-// workspace
-// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-// .await
-// .unwrap();
-
-// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
-// let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
-// assert_eq!(editor.project_path(cx), Some(file1.clone()));
-// let buffer = editor.update(cx, |editor, cx| {
-// editor.insert("dirt", cx);
-// editor.buffer().downgrade()
-// });
-// (editor.downgrade(), buffer)
-// });
-
-// cx.dispatch_action(window.into(), pane::SplitRight);
-// let editor_2 = cx.update(|cx| {
-// let pane_2 = workspace.read(cx).active_pane().clone();
-// assert_ne!(pane_1, pane_2);
-
-// let pane2_item = pane_2.read(cx).active_item().unwrap();
-// assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
-
-// pane2_item.downcast::<Editor>().unwrap().downgrade()
-// });
-// cx.dispatch_action(
-// window.into(),
-// workspace::CloseActiveItem { save_intent: None },
-// );
-
-// cx.foreground().run_until_parked();
-// workspace.read_with(cx, |workspace, _| {
-// assert_eq!(workspace.panes().len(), 1);
-// assert_eq!(workspace.active_pane(), &pane_1);
-// });
-
-// cx.dispatch_action(
-// window.into(),
-// workspace::CloseActiveItem { save_intent: None },
-// );
-// cx.foreground().run_until_parked();
-// window.simulate_prompt_answer(1, cx);
-// cx.foreground().run_until_parked();
-
-// workspace.read_with(cx, |workspace, cx| {
-// assert_eq!(workspace.panes().len(), 1);
-// assert!(workspace.active_item(cx).is_none());
-// });
-
-// cx.assert_dropped(editor_1);
-// cx.assert_dropped(editor_2);
-// cx.assert_dropped(buffer);
-// }
-
-// #[gpui::test]
-// async fn test_navigation(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {
-// "file1": "contents 1\n".repeat(20),
-// "file2": "contents 2\n".repeat(20),
-// "file3": "contents 3\n".repeat(20),
-// },
-// }),
-// )
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-
-// let entries = cx.read(|cx| workspace.file_project_paths(cx));
-// let file1 = entries[0].clone();
-// let file2 = entries[1].clone();
-// let file3 = entries[2].clone();
-
-// let editor1 = workspace
-// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-// editor1.update(cx, |editor, cx| {
-// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
-// });
-// });
-// let editor2 = workspace
-// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-// let editor3 = workspace
-// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// editor3
-// .update(cx, |editor, cx| {
-// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
-// });
-// editor.newline(&Default::default(), cx);
-// editor.newline(&Default::default(), cx);
-// editor.move_down(&Default::default(), cx);
-// editor.move_down(&Default::default(), cx);
-// editor.save(project.clone(), cx)
-// })
-// .await
-// .unwrap();
-// editor3.update(cx, |editor, cx| {
-// editor.set_scroll_position(vec2f(0., 12.5), cx)
-// });
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file2.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(10, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// // Go back one more time and ensure we don't navigate past the first item in the history.
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(10, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file2.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// // Go forward to an item that has been closed, ensuring it gets re-opened at the same
-// // location.
-// pane.update(cx, |pane, cx| {
-// let editor3_id = editor3.id();
-// drop(editor3);
-// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// workspace
-// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
-// );
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
-// pane.update(cx, |pane, cx| {
-// let editor2_id = editor2.id();
-// drop(editor2);
-// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// app_state
-// .fs
-// .remove_file(Path::new("/root/a/file2"), Default::default())
-// .await
-// .unwrap();
-// cx.foreground().run_until_parked();
-
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(10, 0), 0.)
-// );
-// workspace
-// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file3.clone(), DisplayPoint::new(0, 0), 0.)
-// );
-
-// // Modify file to collapse multiple nav history entries into the same location.
-// // Ensure we don't visit the same location twice when navigating.
-// editor1.update(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
-// })
-// });
-
-// for _ in 0..5 {
-// editor1.update(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
-// });
-// });
-// editor1.update(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
-// })
-// });
-// }
-
-// editor1.update(cx, |editor, cx| {
-// editor.transact(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
-// });
-// editor.insert("", cx);
-// })
-// });
-
-// editor1.update(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
-// })
-// });
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(2, 0), 0.)
-// );
-// workspace
-// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// active_location(&workspace, cx),
-// (file1.clone(), DisplayPoint::new(3, 0), 0.)
-// );
-
-// fn active_location(
-// workspace: &ViewHandle<Workspace>,
-// cx: &mut TestAppContext,
-// ) -> (ProjectPath, DisplayPoint, f32) {
-// workspace.update(cx, |workspace, cx| {
-// let item = workspace.active_item(cx).unwrap();
-// let editor = item.downcast::<Editor>().unwrap();
-// let (selections, scroll_position) = editor.update(cx, |editor, cx| {
-// (
-// editor.selections.display_ranges(cx),
-// editor.scroll_position(cx),
-// )
-// });
-// (
-// item.project_path(cx).unwrap(),
-// selections[0].start,
-// scroll_position.y(),
-// )
-// })
-// }
-// }
-
-// #[gpui::test]
-// async fn test_reopening_closed_items(cx: &mut TestAppContext) {
-// let app_state = init_test(cx);
-// app_state
-// .fs
-// .as_fake()
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {
-// "file1": "",
-// "file2": "",
-// "file3": "",
-// "file4": "",
-// },
-// }),
-// )
-// .await;
-
-// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project, cx))
-// .root(cx);
-// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-
-// let entries = cx.read(|cx| workspace.file_project_paths(cx));
-// let file1 = entries[0].clone();
-// let file2 = entries[1].clone();
-// let file3 = entries[2].clone();
-// let file4 = entries[3].clone();
-
-// let file1_item_id = workspace
-// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .id();
-// let file2_item_id = workspace
-// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .id();
-// let file3_item_id = workspace
-// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .id();
-// let file4_item_id = workspace
-// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
-// .await
-// .unwrap()
-// .id();
-// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-// // Close all the pane items in some arbitrary order.
-// pane.update(cx, |pane, cx| {
-// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-// pane.update(cx, |pane, cx| {
-// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-// pane.update(cx, |pane, cx| {
-// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-// pane.update(cx, |pane, cx| {
-// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), None);
-
-// // Reopen all the closed items, ensuring they are reopened in the same order
-// // in which they were closed.
-// workspace
-// .update(cx, Workspace::reopen_closed_item)
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-// workspace
-// .update(cx, Workspace::reopen_closed_item)
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-// workspace
-// .update(cx, Workspace::reopen_closed_item)
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-// workspace
-// .update(cx, Workspace::reopen_closed_item)
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-// // Reopening past the last closed item is a no-op.
-// workspace
-// .update(cx, Workspace::reopen_closed_item)
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-// // Reopening closed items doesn't interfere with navigation history.
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.go_back(workspace.active_pane().downgrade(), cx)
-// })
-// .await
-// .unwrap();
-// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-// fn active_path(
-// workspace: &ViewHandle<Workspace>,
-// cx: &TestAppContext,
-// ) -> Option<ProjectPath> {
-// workspace.read_with(cx, |workspace, cx| {
-// let item = workspace.active_item(cx)?;
-// item.project_path(cx)
-// })
-// }
-// }
-
-// #[gpui::test]
-// async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
-// struct TestView;
-
-// impl Entity for TestView {
-// type Event = ();
-// }
-
-// impl View for TestView {
-// fn ui_name() -> &'static str {
-// "TestView"
-// }
-
-// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-// Empty::new().into_any()
-// }
-// }
-
-// let executor = cx.background();
-// let fs = FakeFs::new(executor.clone());
-
-// actions!(test, [A, B]);
-// // From the Atom keymap
-// actions!(workspace, [ActivatePreviousPane]);
-// // From the JetBrains keymap
-// actions!(pane, [ActivatePrevItem]);
-
-// fs.save(
-// "/settings.json".as_ref(),
-// &r#"
-// {
-// "base_keymap": "Atom"
-// }
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// fs.save(
-// "/keymap.json".as_ref(),
-// &r#"
-// [
-// {
-// "bindings": {
-// "backspace": "test::A"
-// }
-// }
-// ]
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.update(|cx| {
-// cx.set_global(SettingsStore::test(cx));
-// theme::init(Assets, cx);
-// welcome::init(cx);
-
-// cx.add_global_action(|_: &A, _cx| {});
-// cx.add_global_action(|_: &B, _cx| {});
-// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
-// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-
-// let settings_rx = watch_config_file(
-// executor.clone(),
-// fs.clone(),
-// PathBuf::from("/settings.json"),
-// );
-// let keymap_rx =
-// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
-
-// handle_keymap_file_changes(keymap_rx, cx);
-// handle_settings_file_changes(settings_rx, cx);
-// });
-
-// cx.foreground().run_until_parked();
-
-// let window = cx.add_window(|_| TestView);
-
-// // Test loading the keymap base at all
-// assert_key_bindings_for(
-// window.into(),
-// cx,
-// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
-// line!(),
-// );
-
-// // Test modifying the users keymap, while retaining the base keymap
-// fs.save(
-// "/keymap.json".as_ref(),
-// &r#"
-// [
-// {
-// "bindings": {
-// "backspace": "test::B"
-// }
-// }
-// ]
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-
-// assert_key_bindings_for(
-// window.into(),
-// cx,
-// vec![("backspace", &B), ("k", &ActivatePreviousPane)],
-// line!(),
-// );
-
-// // Test modifying the base, while retaining the users keymap
-// fs.save(
-// "/settings.json".as_ref(),
-// &r#"
-// {
-// "base_keymap": "JetBrains"
-// }
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-
-// assert_key_bindings_for(
-// window.into(),
-// cx,
-// vec![("backspace", &B), ("[", &ActivatePrevItem)],
-// line!(),
-// );
-
-// #[track_caller]
-// fn assert_key_bindings_for<'a>(
-// window: AnyWindowHandle,
-// cx: &TestAppContext,
-// actions: Vec<(&'static str, &'a dyn Action)>,
-// line: u32,
-// ) {
-// for (key, action) in actions {
-// // assert that...
-// assert!(
-// cx.available_actions(window, 0)
-// .into_iter()
-// .any(|(_, bound_action, b)| {
-// // action names match...
-// bound_action.name() == action.name()
-// && bound_action.namespace() == action.namespace()
-// // and key strokes contain the given key
-// && b.iter()
-// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
-// }),
-// "On {} Failed to find {} with key binding {}",
-// line,
-// action.name(),
-// key
-// );
-// }
-// }
-// }
-
-// #[gpui::test]
-// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
-// struct TestView;
-
-// impl Entity for TestView {
-// type Event = ();
-// }
-
-// impl View for TestView {
-// fn ui_name() -> &'static str {
-// "TestView"
-// }
-
-// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-// Empty::new().into_any()
-// }
-// }
-
-// let executor = cx.background();
-// let fs = FakeFs::new(executor.clone());
-
-// actions!(test, [A, B]);
-// // From the Atom keymap
-// actions!(workspace, [ActivatePreviousPane]);
-// // From the JetBrains keymap
-// actions!(pane, [ActivatePrevItem]);
-
-// fs.save(
-// "/settings.json".as_ref(),
-// &r#"
-// {
-// "base_keymap": "Atom"
-// }
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// fs.save(
-// "/keymap.json".as_ref(),
-// &r#"
-// [
-// {
-// "bindings": {
-// "backspace": "test::A"
-// }
-// }
-// ]
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.update(|cx| {
-// cx.set_global(SettingsStore::test(cx));
-// theme::init(Assets, cx);
-// welcome::init(cx);
-
-// cx.add_global_action(|_: &A, _cx| {});
-// cx.add_global_action(|_: &B, _cx| {});
-// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
-// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-
-// let settings_rx = watch_config_file(
-// executor.clone(),
-// fs.clone(),
-// PathBuf::from("/settings.json"),
-// );
-// let keymap_rx =
-// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
-
-// handle_keymap_file_changes(keymap_rx, cx);
-// handle_settings_file_changes(settings_rx, cx);
-// });
-
-// cx.foreground().run_until_parked();
-
-// let window = cx.add_window(|_| TestView);
-
-// // Test loading the keymap base at all
-// assert_key_bindings_for(
-// window.into(),
-// cx,
-// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
-// line!(),
-// );
-
-// // Test disabling the key binding for the base keymap
-// fs.save(
-// "/keymap.json".as_ref(),
-// &r#"
-// [
-// {
-// "bindings": {
-// "backspace": null
-// }
-// }
-// ]
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-
-// assert_key_bindings_for(
-// window.into(),
-// cx,
-// vec![("k", &ActivatePreviousPane)],
-// line!(),
-// );
-
-// // Test modifying the base, while retaining the users keymap
-// fs.save(
-// "/settings.json".as_ref(),
-// &r#"
-// {
-// "base_keymap": "JetBrains"
-// }
-// "#
-// .into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-
-// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
-
-// #[track_caller]
-// fn assert_key_bindings_for<'a>(
-// window: AnyWindowHandle,
-// cx: &TestAppContext,
-// actions: Vec<(&'static str, &'a dyn Action)>,
-// line: u32,
-// ) {
-// for (key, action) in actions {
-// // assert that...
-// assert!(
-// cx.available_actions(window, 0)
-// .into_iter()
-// .any(|(_, bound_action, b)| {
-// // action names match...
-// bound_action.name() == action.name()
-// && bound_action.namespace() == action.namespace()
-// // and key strokes contain the given key
-// && b.iter()
-// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
-// }),
-// "On {} Failed to find {} with key binding {}",
-// line,
-// action.name(),
-// key
-// );
-// }
-// }
-// }
-
-// #[gpui::test]
-// fn test_bundled_settings_and_themes(cx: &mut AppContext) {
-// cx.platform()
-// .fonts()
-// .add_fonts(&[
-// Assets
-// .load("fonts/zed-sans/zed-sans-extended.ttf")
-// .unwrap()
-// .to_vec()
-// .into(),
-// Assets
-// .load("fonts/zed-mono/zed-mono-extended.ttf")
-// .unwrap()
-// .to_vec()
-// .into(),
-// Assets
-// .load("fonts/plex/IBMPlexSans-Regular.ttf")
-// .unwrap()
-// .to_vec()
-// .into(),
-// ])
-// .unwrap();
-// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-// let mut settings = SettingsStore::default();
-// settings
-// .set_default_settings(&settings::default_settings(), cx)
-// .unwrap();
-// cx.set_global(settings);
-// theme::init(Assets, cx);
-
-// let mut has_default_theme = false;
-// for theme_name in themes.list(false).map(|meta| meta.name) {
-// let theme = themes.get(&theme_name).unwrap();
-// assert_eq!(theme.meta.name, theme_name);
-// if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
-// has_default_theme = true;
-// }
-// }
-// assert!(has_default_theme);
-// }
-
-// #[gpui::test]
-// fn test_bundled_languages(cx: &mut AppContext) {
-// cx.set_global(SettingsStore::test(cx));
-// let mut languages = LanguageRegistry::test();
-// languages.set_executor(cx.background().clone());
-// let languages = Arc::new(languages);
-// let node_runtime = node_runtime::FakeNodeRuntime::new();
-// languages::init(languages.clone(), node_runtime, cx);
-// for name in languages.language_names() {
-// languages.language_for_name(&name);
-// }
-// cx.foreground().run_until_parked();
-// }
-
-// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
-// cx.foreground().forbid_parking();
-// cx.update(|cx| {
-// let mut app_state = AppState::test(cx);
-// let state = Arc::get_mut(&mut app_state).unwrap();
-// state.initialize_workspace = initialize_workspace;
-// state.build_window_options = build_window_options;
-// theme::init((), cx);
-// audio::init((), cx);
-// channel::init(&app_state.client, app_state.user_store.clone(), cx);
-// call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-// workspace::init(app_state.clone(), cx);
-// Project::init_settings(cx);
-// language::init(cx);
-// editor::init(cx);
-// project_panel::init_settings(cx);
-// collab_ui::init(&app_state, cx);
-// pane::init(cx);
-// project_panel::init((), cx);
-// terminal_view::init(cx);
-// assistant::init(cx);
-// app_state
-// })
-// }
-
-// fn rust_lang() -> Arc<language::Language> {
-// Arc::new(language::Language::new(
-// language::LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// ))
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use assets::Assets;
+ use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor, EditorEvent};
+ use gpui::{
+ actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
+ VisualTestContext, WindowHandle,
+ };
+ use language::LanguageRegistry;
+ use project::{project_settings::ProjectSettings, Project, ProjectPath};
+ use serde_json::json;
+ use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
+ use std::{
+ collections::HashSet,
+ path::{Path, PathBuf},
+ };
+ use theme::{ThemeRegistry, ThemeSettings};
+ use workspace::{
+ item::{Item, ItemHandle},
+ open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
+ WorkspaceHandle,
+ };
+
+ // #[gpui::test]
+ // async fn test_open_paths_action(cx: &mut TestAppContext) {
+ // let app_state = init_test(cx);
+ // app_state
+ // .fs
+ // .as_fake()
+ // .insert_tree(
+ // "/root",
+ // json!({
+ // "a": {
+ // "aa": null,
+ // "ab": null,
+ // },
+ // "b": {
+ // "ba": null,
+ // "bb": null,
+ // },
+ // "c": {
+ // "ca": null,
+ // "cb": null,
+ // },
+ // "d": {
+ // "da": null,
+ // "db": null,
+ // },
+ // }),
+ // )
+ // .await;
+
+ // cx.update(|cx| {
+ // open_paths(
+ // &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
+ // &app_state,
+ // None,
+ // cx,
+ // )
+ // })
+ // .await
+ // .unwrap();
+ // assert_eq!(cx.read(|cx| cx.windows().len()), 1);
+
+ // cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+ // .await
+ // .unwrap();
+ // assert_eq!(cx.read(|cx| cx.windows().len()), 1);
+ // let workspace_1 = cx
+ // .read(|cx| cx.windows()[0].downcast::<Workspace>())
+ // .unwrap();
+ // workspace_1
+ // .update(cx, |workspace, cx| {
+ // assert_eq!(workspace.worktrees(cx).count(), 2);
+ // assert!(workspace.left_dock().read(cx).is_open());
+ // assert!(workspace
+ // .active_pane()
+ // .read(cx)
+ // .focus_handle(cx)
+ // .is_focused(cx));
+ // })
+ // .unwrap();
+
+ // cx.update(|cx| {
+ // open_paths(
+ // &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+ // &app_state,
+ // None,
+ // cx,
+ // )
+ // })
+ // .await
+ // .unwrap();
+ // assert_eq!(cx.read(|cx| cx.windows().len()), 2);
+
+ // // Replace existing windows
+ // let window = cx
+ // .update(|cx| cx.windows()[0].downcast::<Workspace>())
+ // .unwrap();
+ // cx.update(|cx| {
+ // open_paths(
+ // &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+ // &app_state,
+ // Some(window),
+ // cx,
+ // )
+ // })
+ // .await
+ // .unwrap();
+ // assert_eq!(cx.read(|cx| cx.windows().len()), 2);
+ // let workspace_1 = cx
+ // .update(|cx| cx.windows()[0].downcast::<Workspace>())
+ // .unwrap();
+ // workspace_1
+ // .update(cx, |workspace, cx| {
+ // assert_eq!(
+ // workspace
+ // .worktrees(cx)
+ // .map(|w| w.read(cx).abs_path())
+ // .collect::<Vec<_>>(),
+ // &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+ // );
+ // assert!(workspace.left_dock().read(cx).is_open());
+ // assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
+ // })
+ // .unwrap();
+ // }
+
+ #[gpui::test]
+ async fn test_window_edit_state(cx: &mut TestAppContext) {
+ let executor = cx.executor();
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree("/root", json!({"a": "hey"}))
+ .await;
+
+ cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+ .await
+ .unwrap();
+ assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+ // When opening the workspace, the window is not in a edited state.
+ let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+
+ let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
+ cx.test_window(window.into()).edited()
+ };
+ let pane = window
+ .read_with(cx, |workspace, _| workspace.active_pane().clone())
+ .unwrap();
+ let editor = window
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ })
+ .unwrap();
+
+ assert!(!window_is_edited(window, cx));
+
+ // Editing a buffer marks the window as edited.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+ })
+ .unwrap();
+
+ assert!(window_is_edited(window, cx));
+
+ // Undoing the edit restores the window's edited state.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
+ })
+ .unwrap();
+ assert!(!window_is_edited(window, cx));
+
+ // Redoing the edit marks the window as edited again.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
+ })
+ .unwrap();
+ assert!(window_is_edited(window, cx));
+
+ // Closing the item restores the window's edited state.
+ let close = window
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ drop(editor);
+ pane.close_active_item(&Default::default(), cx).unwrap()
+ })
+ })
+ .unwrap();
+ executor.run_until_parked();
+
+ cx.simulate_prompt_answer(1);
+ close.await.unwrap();
+ assert!(!window_is_edited(window, cx));
+
+ // Opening the buffer again doesn't impact the window's edited state.
+ cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+ .await
+ .unwrap();
+ let editor = window
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ })
+ .unwrap();
+ assert!(!window_is_edited(window, cx));
+
+ // Editing the buffer marks the window as edited.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+ })
+ .unwrap();
+ assert!(window_is_edited(window, cx));
+
+ // Ensure closing the window via the mouse gets preempted due to the
+ // buffer having unsaved changes.
+ assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
+ executor.run_until_parked();
+ assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+ // The window is successfully closed after the user dismisses the prompt.
+ cx.simulate_prompt_answer(1);
+ executor.run_until_parked();
+ assert_eq!(cx.update(|cx| cx.windows().len()), 0);
+ }
+
+ #[gpui::test]
+ async fn test_new_empty_workspace(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ cx.update(|cx| {
+ open_new(&app_state, cx, |workspace, cx| {
+ Editor::new_file(workspace, &Default::default(), cx)
+ })
+ })
+ .await;
+
+ let workspace = cx
+ .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
+ .unwrap();
+
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ let editor = workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<editor::Editor>()
+ .unwrap();
+ editor.update(cx, |editor, cx| {
+ assert!(editor.text(cx).is_empty());
+ assert!(!editor.is_dirty(cx));
+ });
+
+ editor
+ })
+ .unwrap();
+
+ let save_task = workspace
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(SaveIntent::Save, cx)
+ })
+ .unwrap();
+ app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+ cx.background_executor.run_until_parked();
+ cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
+ save_task.await.unwrap();
+ workspace
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert_eq!(editor.title(cx), "the-new-name");
+ });
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_open_entry(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {
+ "file1": "contents 1",
+ "file2": "contents 2",
+ "file3": "contents 3",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let entries = cx.read(|cx| workspace.file_project_paths(cx));
+ let file1 = entries[0].clone();
+ let file2 = entries[1].clone();
+ let file3 = entries[2].clone();
+
+ // Open the first entry
+ let entry_1 = window
+ .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap();
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ assert_eq!(
+ pane.active_item().unwrap().project_path(cx),
+ Some(file1.clone())
+ );
+ assert_eq!(pane.items_len(), 1);
+ });
+
+ // Open the second entry
+ window
+ .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap();
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ assert_eq!(
+ pane.active_item().unwrap().project_path(cx),
+ Some(file2.clone())
+ );
+ assert_eq!(pane.items_len(), 2);
+ });
+
+ // Open the first entry again. The existing pane item is activated.
+ let entry_1b = window
+ .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(entry_1.item_id(), entry_1b.item_id());
+
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ assert_eq!(
+ pane.active_item().unwrap().project_path(cx),
+ Some(file1.clone())
+ );
+ assert_eq!(pane.items_len(), 2);
+ });
+
+ // Split the pane with the first entry, then open the second entry again.
+ window
+ .update(cx, |w, cx| {
+ w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
+ w.open_path(file2.clone(), None, true, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ window
+ .read_with(cx, |w, cx| {
+ assert_eq!(
+ w.active_pane()
+ .read(cx)
+ .active_item()
+ .unwrap()
+ .project_path(cx),
+ Some(file2.clone())
+ );
+ })
+ .unwrap();
+
+ // Open the third entry twice concurrently. Only one pane item is added.
+ let (t1, t2) = window
+ .update(cx, |w, cx| {
+ (
+ w.open_path(file3.clone(), None, true, cx),
+ w.open_path(file3.clone(), None, true, cx),
+ )
+ })
+ .unwrap();
+ t1.await.unwrap();
+ t2.await.unwrap();
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ assert_eq!(
+ pane.active_item().unwrap().project_path(cx),
+ Some(file3.clone())
+ );
+ let pane_entries = pane
+ .items()
+ .map(|i| i.project_path(cx).unwrap())
+ .collect::<Vec<_>>();
+ assert_eq!(pane_entries, &[file1, file2, file3]);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_open_paths(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/",
+ json!({
+ "dir1": {
+ "a.txt": ""
+ },
+ "dir2": {
+ "b.txt": ""
+ },
+ "dir3": {
+ "c.txt": ""
+ },
+ "d.txt": ""
+ }),
+ )
+ .await;
+
+ cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+ .await
+ .unwrap();
+ assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+ let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+ let workspace = window.root(cx).unwrap();
+
+ #[track_caller]
+ fn assert_project_panel_selection(
+ workspace: &Workspace,
+ expected_worktree_path: &Path,
+ expected_entry_path: &Path,
+ cx: &AppContext,
+ ) {
+ let project_panel = [
+ workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+ workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+ workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+ ]
+ .into_iter()
+ .find_map(std::convert::identity)
+ .expect("found no project panels")
+ .read(cx);
+ let (selected_worktree, selected_entry) = project_panel
+ .selected_entry(cx)
+ .expect("project panel should have a selected entry");
+ assert_eq!(
+ selected_worktree.abs_path().as_ref(),
+ expected_worktree_path,
+ "Unexpected project panel selected worktree path"
+ );
+ assert_eq!(
+ selected_entry.path.as_ref(),
+ expected_entry_path,
+ "Unexpected project panel selected entry path"
+ );
+ }
+
+ // Open a file within an existing worktree.
+ window
+ .update(cx, |view, cx| {
+ view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx)
+ })
+ .unwrap()
+ .await;
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
+ assert_eq!(
+ workspace
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .unwrap()
+ .act_as::<Editor>(cx)
+ .unwrap()
+ .read(cx)
+ .title(cx),
+ "a.txt"
+ );
+ });
+
+ // Open a file outside of any existing worktree.
+ window
+ .update(cx, |view, cx| {
+ view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx)
+ })
+ .unwrap()
+ .await;
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
+ let worktree_roots = workspace
+ .worktrees(cx)
+ .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ worktree_roots,
+ vec!["/dir1", "/dir2/b.txt"]
+ .into_iter()
+ .map(Path::new)
+ .collect(),
+ );
+ assert_eq!(
+ workspace
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .unwrap()
+ .act_as::<Editor>(cx)
+ .unwrap()
+ .read(cx)
+ .title(cx),
+ "b.txt"
+ );
+ });
+
+ // Ensure opening a directory and one of its children only adds one worktree.
+ window
+ .update(cx, |view, cx| {
+ view.open_paths(
+ vec!["/dir3".into(), "/dir3/c.txt".into()],
+ OpenVisible::All,
+ None,
+ cx,
+ )
+ })
+ .unwrap()
+ .await;
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
+ let worktree_roots = workspace
+ .worktrees(cx)
+ .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ worktree_roots,
+ vec!["/dir1", "/dir2/b.txt", "/dir3"]
+ .into_iter()
+ .map(Path::new)
+ .collect(),
+ );
+ assert_eq!(
+ workspace
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .unwrap()
+ .act_as::<Editor>(cx)
+ .unwrap()
+ .read(cx)
+ .title(cx),
+ "c.txt"
+ );
+ });
+
+ // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
+ window
+ .update(cx, |view, cx| {
+ view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx)
+ })
+ .unwrap()
+ .await;
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
+ let worktree_roots = workspace
+ .worktrees(cx)
+ .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ worktree_roots,
+ vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
+ .into_iter()
+ .map(Path::new)
+ .collect(),
+ );
+
+ let visible_worktree_roots = workspace
+ .visible_worktrees(cx)
+ .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ visible_worktree_roots,
+ vec!["/dir1", "/dir2/b.txt", "/dir3"]
+ .into_iter()
+ .map(Path::new)
+ .collect(),
+ );
+
+ assert_eq!(
+ workspace
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .unwrap()
+ .act_as::<Editor>(cx)
+ .unwrap()
+ .read(cx)
+ .title(cx),
+ "d.txt"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+ });
+ });
+ });
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "ignored_dir\n",
+ ".git": {
+ "HEAD": "ref: refs/heads/main",
+ },
+ "regular_dir": {
+ "file": "regular file contents",
+ },
+ "ignored_dir": {
+ "ignored_subdir": {
+ "file": "ignored subfile contents",
+ },
+ "file": "ignored file contents",
+ },
+ "excluded_dir": {
+ "file": "excluded file contents",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+ let paths_to_open = [
+ Path::new("/root/excluded_dir/file").to_path_buf(),
+ Path::new("/root/.git/HEAD").to_path_buf(),
+ Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+ ];
+ let (opened_workspace, new_items) = cx
+ .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+ .await
+ .unwrap();
+
+ assert_eq!(
+ opened_workspace.root_view(cx).unwrap().entity_id(),
+ workspace.entity_id(),
+ "Excluded files in subfolders of a workspace root should be opened in the workspace"
+ );
+ let mut opened_paths = cx.read(|cx| {
+ assert_eq!(
+ new_items.len(),
+ paths_to_open.len(),
+ "Expect to get the same number of opened items as submitted paths to open"
+ );
+ new_items
+ .iter()
+ .zip(paths_to_open.iter())
+ .map(|(i, path)| {
+ match i {
+ Some(Ok(i)) => {
+ Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+ }
+ Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+ None => None,
+ }
+ .flatten()
+ })
+ .collect::<Vec<_>>()
+ });
+ opened_paths.sort();
+ assert_eq!(
+ opened_paths,
+ vec![
+ None,
+ Some(".git/HEAD".to_string()),
+ Some("excluded_dir/file".to_string()),
+ ],
+ "Excluded files should get opened, excluded dir should not get opened"
+ );
+
+ let entries = cx.read(|cx| workspace.file_project_paths(cx));
+ assert_eq!(
+ initial_entries, entries,
+ "Workspace entries should not change after opening excluded files and directories paths"
+ );
+
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ let mut opened_buffer_paths = pane
+ .items()
+ .map(|i| {
+ i.project_path(cx)
+ .expect("all excluded files that got open should have a path")
+ .path
+ .display()
+ .to_string()
+ })
+ .collect::<Vec<_>>();
+ opened_buffer_paths.sort();
+ assert_eq!(
+ opened_buffer_paths,
+ vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+ "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_save_conflicting_item(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree("/root", json!({ "a.txt": "" }))
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx).unwrap();
+
+ // Open a file within an existing worktree.
+ window
+ .update(cx, |view, cx| {
+ view.open_paths(
+ vec![PathBuf::from("/root/a.txt")],
+ OpenVisible::All,
+ None,
+ cx,
+ )
+ })
+ .unwrap()
+ .await;
+ let editor = cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ let item = pane.active_item().unwrap();
+ item.downcast::<Editor>().unwrap()
+ });
+
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| editor.handle_input("x", cx));
+ })
+ .unwrap();
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_file("/root/a.txt", "changed".to_string())
+ .await;
+ editor
+ .condition::<EditorEvent>(cx, |editor, cx| editor.has_conflict(cx))
+ .await;
+ cx.read(|cx| assert!(editor.is_dirty(cx)));
+
+ let save_task = window
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(SaveIntent::Save, cx)
+ })
+ .unwrap();
+ cx.background_executor.run_until_parked();
+ cx.simulate_prompt_answer(0);
+ save_task.await.unwrap();
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert!(!editor.has_conflict(cx));
+ });
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(rust_lang()));
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
+
+ // Create a new untitled buffer
+ cx.dispatch_action(window.into(), NewFile);
+ let editor = window
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ })
+ .unwrap();
+
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert_eq!(editor.title(cx), "untitled");
+ assert!(Arc::ptr_eq(
+ &editor.buffer().read(cx).language_at(0, cx).unwrap(),
+ &languages::PLAIN_TEXT
+ ));
+ editor.handle_input("hi", cx);
+ assert!(editor.is_dirty(cx));
+ });
+ })
+ .unwrap();
+
+ // Save the buffer. This prompts for a filename.
+ let save_task = window
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(SaveIntent::Save, cx)
+ })
+ .unwrap();
+ cx.background_executor.run_until_parked();
+ cx.simulate_new_path_selection(|parent_dir| {
+ assert_eq!(parent_dir, Path::new("/root"));
+ Some(parent_dir.join("the-new-name.rs"))
+ });
+ cx.read(|cx| {
+ assert!(editor.is_dirty(cx));
+ assert_eq!(editor.read(cx).title(cx), "untitled");
+ });
+
+ // When the save completes, the buffer's title is updated and the language is assigned based
+ // on the path.
+ save_task.await.unwrap();
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert_eq!(editor.title(cx), "the-new-name.rs");
+ assert_eq!(
+ editor
+ .buffer()
+ .read(cx)
+ .language_at(0, cx)
+ .unwrap()
+ .name()
+ .as_ref(),
+ "Rust"
+ );
+ });
+ })
+ .unwrap();
+
+ // Edit the file and save it again. This time, there is no filename prompt.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.handle_input(" there", cx);
+ assert!(editor.is_dirty(cx));
+ });
+ })
+ .unwrap();
+
+ let save_task = window
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(SaveIntent::Save, cx)
+ })
+ .unwrap();
+ save_task.await.unwrap();
+ // todo!() po
+ //assert!(!cx.did_prompt_for_new_path());
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert_eq!(editor.title(cx), "the-new-name.rs")
+ });
+ })
+ .unwrap();
+
+ // Open the same newly-created file in another pane item. The new editor should reuse
+ // the same buffer.
+ cx.dispatch_action(window.into(), NewFile);
+ window
+ .update(cx, |workspace, cx| {
+ workspace.split_and_clone(
+ workspace.active_pane().clone(),
+ SplitDirection::Right,
+ cx,
+ );
+ workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ let editor2 = window
+ .update(cx, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ })
+ .unwrap();
+ cx.read(|cx| {
+ assert_eq!(
+ editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+ editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+ );
+ })
+ }
+
+ #[gpui::test]
+ async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+ let project = Project::test(app_state.fs.clone(), [], cx).await;
+ project.update(cx, |project, _| project.languages().add(rust_lang()));
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+
+ // Create a new untitled buffer
+ cx.dispatch_action(window.into(), NewFile);
+ let editor = window
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ })
+ .unwrap();
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(Arc::ptr_eq(
+ &editor.buffer().read(cx).language_at(0, cx).unwrap(),
+ &languages::PLAIN_TEXT
+ ));
+ editor.handle_input("hi", cx);
+ assert!(editor.is_dirty(cx));
+ });
+ })
+ .unwrap();
+
+ // Save the buffer. This prompts for a filename.
+ let save_task = window
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(SaveIntent::Save, cx)
+ })
+ .unwrap();
+ cx.background_executor.run_until_parked();
+ cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
+ save_task.await.unwrap();
+ // The buffer is not dirty anymore and the language is assigned based on the path.
+ window
+ .update(cx, |_, cx| {
+ editor.update(cx, |editor, cx| {
+ assert!(!editor.is_dirty(cx));
+ assert_eq!(
+ editor
+ .buffer()
+ .read(cx)
+ .language_at(0, cx)
+ .unwrap()
+ .name()
+ .as_ref(),
+ "Rust"
+ )
+ });
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_pane_actions(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {
+ "file1": "contents 1",
+ "file2": "contents 2",
+ "file3": "contents 3",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let entries = cx.read(|cx| workspace.file_project_paths(cx));
+ let file1 = entries[0].clone();
+
+ let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
+
+ window
+ .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap();
+
+ let (editor_1, buffer) = window
+ .update(cx, |_, cx| {
+ pane_1.update(cx, |pane_1, cx| {
+ let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+ assert_eq!(editor.project_path(cx), Some(file1.clone()));
+ let buffer = editor.update(cx, |editor, cx| {
+ editor.insert("dirt", cx);
+ editor.buffer().downgrade()
+ });
+ (editor.downgrade(), buffer)
+ })
+ })
+ .unwrap();
+
+ cx.dispatch_action(window.into(), pane::SplitRight);
+ let editor_2 = cx.update(|cx| {
+ let pane_2 = workspace.read(cx).active_pane().clone();
+ assert_ne!(pane_1, pane_2);
+
+ let pane2_item = pane_2.read(cx).active_item().unwrap();
+ assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
+
+ pane2_item.downcast::<Editor>().unwrap().downgrade()
+ });
+ cx.dispatch_action(
+ window.into(),
+ workspace::CloseActiveItem { save_intent: None },
+ );
+
+ cx.background_executor.run_until_parked();
+ window
+ .read_with(cx, |workspace, _| {
+ assert_eq!(workspace.panes().len(), 1);
+ assert_eq!(workspace.active_pane(), &pane_1);
+ })
+ .unwrap();
+
+ cx.dispatch_action(
+ window.into(),
+ workspace::CloseActiveItem { save_intent: None },
+ );
+ cx.background_executor.run_until_parked();
+ cx.simulate_prompt_answer(1);
+ cx.background_executor.run_until_parked();
+
+ window
+ .read_with(cx, |workspace, cx| {
+ assert_eq!(workspace.panes().len(), 1);
+ assert!(workspace.active_item(cx).is_none());
+ })
+ .unwrap();
+ editor_1.assert_dropped();
+ editor_2.assert_dropped();
+ buffer.assert_dropped();
+ }
+
+ #[gpui::test]
+ async fn test_navigation(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {
+ "file1": "contents 1\n".repeat(20),
+ "file2": "contents 2\n".repeat(20),
+ "file3": "contents 3\n".repeat(20),
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let pane = workspace
+ .read_with(cx, |workspace, _| workspace.active_pane().clone())
+ .unwrap();
+
+ let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
+ let file1 = entries[0].clone();
+ let file2 = entries[1].clone();
+ let file3 = entries[2].clone();
+
+ let editor1 = workspace
+ .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_display_ranges(
+ [DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)],
+ )
+ });
+ });
+ })
+ .unwrap();
+
+ let editor2 = workspace
+ .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let editor3 = workspace
+ .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ workspace
+ .update(cx, |_, cx| {
+ editor3.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_display_ranges(
+ [DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)],
+ )
+ });
+ editor.newline(&Default::default(), cx);
+ editor.newline(&Default::default(), cx);
+ editor.move_down(&Default::default(), cx);
+ editor.move_down(&Default::default(), cx);
+ editor.save(project.clone(), cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ workspace
+ .update(cx, |_, cx| {
+ editor3.update(cx, |editor, cx| {
+ editor.set_scroll_position(point(0., 12.5), cx)
+ });
+ })
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file2.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(10, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ // Go back one more time and ensure we don't navigate past the first item in the history.
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(10, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file2.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+ // location.
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ let editor3_id = editor3.entity_id();
+ drop(editor3);
+ pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ workspace
+ .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+ );
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ let editor2_id = editor2.entity_id();
+ drop(editor2);
+ pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ app_state
+ .fs
+ .remove_file(Path::new("/root/a/file2"), Default::default())
+ .await
+ .unwrap();
+ cx.background_executor.run_until_parked();
+
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(10, 0), 0.)
+ );
+ workspace
+ .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file3.clone(), DisplayPoint::new(0, 0), 0.)
+ );
+
+ // Modify file to collapse multiple nav history entries into the same location.
+ // Ensure we don't visit the same location twice when navigating.
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges(
+ [DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)],
+ )
+ })
+ });
+ })
+ .unwrap();
+ for _ in 0..5 {
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)
+ ])
+ });
+ });
+ })
+ .unwrap();
+
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)
+ ])
+ })
+ });
+ })
+ .unwrap();
+ }
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)
+ ])
+ });
+ editor.insert("", cx);
+ })
+ });
+ })
+ .unwrap();
+
+ workspace
+ .update(cx, |_, cx| {
+ editor1.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ })
+ });
+ })
+ .unwrap();
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(2, 0), 0.)
+ );
+ workspace
+ .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ active_location(&workspace, cx),
+ (file1.clone(), DisplayPoint::new(3, 0), 0.)
+ );
+
+ fn active_location(
+ workspace: &WindowHandle<Workspace>,
+ cx: &mut TestAppContext,
+ ) -> (ProjectPath, DisplayPoint, f32) {
+ workspace
+ .update(cx, |workspace, cx| {
+ let item = workspace.active_item(cx).unwrap();
+ let editor = item.downcast::<Editor>().unwrap();
+ let (selections, scroll_position) = editor.update(cx, |editor, cx| {
+ (
+ editor.selections.display_ranges(cx),
+ editor.scroll_position(cx),
+ )
+ });
+ (
+ item.project_path(cx).unwrap(),
+ selections[0].start,
+ scroll_position.y,
+ )
+ })
+ .unwrap()
+ }
+ }
+
+ #[gpui::test]
+ async fn test_reopening_closed_items(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {
+ "file1": "",
+ "file2": "",
+ "file3": "",
+ "file4": "",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let pane = workspace
+ .read_with(cx, |workspace, _| workspace.active_pane().clone())
+ .unwrap();
+
+ let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
+ let file1 = entries[0].clone();
+ let file2 = entries[1].clone();
+ let file3 = entries[2].clone();
+ let file4 = entries[3].clone();
+
+ let file1_item_id = workspace
+ .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .item_id();
+ let file2_item_id = workspace
+ .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .item_id();
+ let file3_item_id = workspace
+ .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .item_id();
+ let file4_item_id = workspace
+ .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
+ .unwrap()
+ .await
+ .unwrap()
+ .item_id();
+ assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+ // Close all the pane items in some arbitrary order.
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+ workspace
+ .update(cx, |_, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ assert_eq!(active_path(&workspace, cx), None);
+
+ // Reopen all the closed items, ensuring they are reopened in the same order
+ // in which they were closed.
+ workspace
+ .update(cx, Workspace::reopen_closed_item)
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+ workspace
+ .update(cx, Workspace::reopen_closed_item)
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+ workspace
+ .update(cx, Workspace::reopen_closed_item)
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+ workspace
+ .update(cx, Workspace::reopen_closed_item)
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+ // Reopening past the last closed item is a no-op.
+ workspace
+ .update(cx, Workspace::reopen_closed_item)
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+ // Reopening closed items doesn't interfere with navigation history.
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+ fn active_path(
+ workspace: &WindowHandle<Workspace>,
+ cx: &TestAppContext,
+ ) -> Option<ProjectPath> {
+ workspace
+ .read_with(cx, |workspace, cx| {
+ let item = workspace.active_item(cx)?;
+ item.project_path(cx)
+ })
+ .unwrap()
+ }
+ }
+ fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
+ cx.update(|cx| {
+ let app_state = AppState::test(cx);
+
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init(&app_state.client, cx);
+ language::init(cx);
+ workspace::init(app_state.clone(), cx);
+ welcome::init(cx);
+ Project::init_settings(cx);
+ app_state
+ })
+ }
+ #[gpui::test]
+ async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+ let executor = cx.executor();
+ let app_state = init_keymap_test(cx);
+ let project = Project::test(app_state.fs.clone(), [], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+ actions!(test1, [A, B]);
+ // From the Atom keymap
+ use workspace::ActivatePreviousPane;
+ // From the JetBrains keymap
+ use workspace::ActivatePrevItem;
+
+ app_state
+ .fs
+ .save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "Atom"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ app_state
+ .fs
+ .save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test1::A"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ cx.update(|cx| {
+ let settings_rx = watch_config_file(
+ &executor,
+ app_state.fs.clone(),
+ PathBuf::from("/settings.json"),
+ );
+ let keymap_rx = watch_config_file(
+ &executor,
+ app_state.fs.clone(),
+ PathBuf::from("/keymap.json"),
+ );
+ handle_settings_file_changes(settings_rx, cx);
+ handle_keymap_file_changes(keymap_rx, cx);
+ });
+ workspace
+ .update(cx, |workspace, _| {
+ workspace.register_action(|_, _: &A, _cx| {});
+ workspace.register_action(|_, _: &B, _cx| {});
+ workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {});
+ workspace.register_action(|_, _: &ActivatePrevItem, _cx| {});
+ })
+ .unwrap();
+ executor.run_until_parked();
+ // Test loading the keymap base at all
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test modifying the users keymap, while retaining the base keymap
+ app_state
+ .fs
+ .save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test1::B"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test modifying the base, while retaining the users keymap
+ app_state
+ .fs
+ .save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "JetBrains"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("backspace", &B), ("[", &ActivatePrevItem)],
+ line!(),
+ );
+ }
+
+ #[gpui::test]
+ async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+ let executor = cx.executor();
+ let app_state = init_keymap_test(cx);
+ let project = Project::test(app_state.fs.clone(), [], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+ actions!(test2, [A, B]);
+ // From the Atom keymap
+ use workspace::ActivatePreviousPane;
+ // From the JetBrains keymap
+ use pane::ActivatePrevItem;
+ workspace
+ .update(cx, |workspace, _| {
+ workspace
+ .register_action(|_, _: &A, _| {})
+ .register_action(|_, _: &B, _| {});
+ })
+ .unwrap();
+ app_state
+ .fs
+ .save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "Atom"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ app_state
+ .fs
+ .save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test2::A"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ let settings_rx = watch_config_file(
+ &executor,
+ app_state.fs.clone(),
+ PathBuf::from("/settings.json"),
+ );
+ let keymap_rx = watch_config_file(
+ &executor,
+ app_state.fs.clone(),
+ PathBuf::from("/keymap.json"),
+ );
+
+ handle_settings_file_changes(settings_rx, cx);
+ handle_keymap_file_changes(keymap_rx, cx);
+ });
+
+ cx.background_executor.run_until_parked();
+
+ cx.background_executor.run_until_parked();
+ // Test loading the keymap base at all
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test disabling the key binding for the base keymap
+ app_state
+ .fs
+ .save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": null
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.background_executor.run_until_parked();
+
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test modifying the base, while retaining the users keymap
+ app_state
+ .fs
+ .save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "JetBrains"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.background_executor.run_until_parked();
+
+ assert_key_bindings_for(
+ workspace.into(),
+ cx,
+ vec![("[", &ActivatePrevItem)],
+ line!(),
+ );
+ }
+
+ #[gpui::test]
+ fn test_bundled_settings_and_themes(cx: &mut AppContext) {
+ cx.text_system()
+ .add_fonts(&[
+ Assets
+ .load("fonts/zed-sans/zed-sans-extended.ttf")
+ .unwrap()
+ .to_vec()
+ .into(),
+ Assets
+ .load("fonts/zed-mono/zed-mono-extended.ttf")
+ .unwrap()
+ .to_vec()
+ .into(),
+ Assets
+ .load("fonts/plex/IBMPlexSans-Regular.ttf")
+ .unwrap()
+ .to_vec()
+ .into(),
+ ])
+ .unwrap();
+ let themes = ThemeRegistry::default();
+ let mut settings = SettingsStore::default();
+ settings
+ .set_default_settings(&settings::default_settings(), cx)
+ .unwrap();
+ cx.set_global(settings);
+ theme::init(theme::LoadThemes::JustBase, cx);
+
+ let mut has_default_theme = false;
+ for theme_name in themes.list(false).map(|meta| meta.name) {
+ let theme = themes.get(&theme_name).unwrap();
+ assert_eq!(theme.name, theme_name);
+ if theme.name == ThemeSettings::get(None, cx).active_theme.name {
+ has_default_theme = true;
+ }
+ }
+ assert!(has_default_theme);
+ }
+
+ #[gpui::test]
+ fn test_bundled_languages(cx: &mut AppContext) {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ let mut languages = LanguageRegistry::test();
+ languages.set_executor(cx.background_executor().clone());
+ let languages = Arc::new(languages);
+ let node_runtime = node_runtime::FakeNodeRuntime::new();
+ languages::init(languages.clone(), node_runtime, cx);
+ for name in languages.language_names() {
+ languages.language_for_name(&name);
+ }
+ cx.background_executor().run_until_parked();
+ }
+
+ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+ cx.update(|cx| {
+ let mut app_state = AppState::test(cx);
+
+ let state = Arc::get_mut(&mut app_state).unwrap();
+
+ state.build_window_options = build_window_options;
+ theme::init(theme::LoadThemes::JustBase, cx);
+ audio::init((), cx);
+ channel::init(&app_state.client, app_state.user_store.clone(), cx);
+ call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+ notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ language::init(cx);
+ editor::init(cx);
+ project_panel::init_settings(cx);
+ collab_ui::init(&app_state, cx);
+ project_panel::init((), cx);
+ terminal_view::init(cx);
+ assistant::init(cx);
+ initialize_workspace(app_state.clone(), cx);
+ app_state
+ })
+ }
+
+ fn rust_lang() -> Arc<language::Language> {
+ Arc::new(language::Language::new(
+ language::LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ))
+ }
+ #[track_caller]
+ fn assert_key_bindings_for<'a>(
+ window: AnyWindowHandle,
+ cx: &TestAppContext,
+ actions: Vec<(&'static str, &'a dyn Action)>,
+ line: u32,
+ ) {
+ let available_actions = cx
+ .update(|cx| window.update(cx, |_, cx| cx.available_actions()))
+ .unwrap();
+ for (key, action) in actions {
+ let bindings = cx
+ .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action)))
+ .unwrap();
+ // assert that...
+ assert!(
+ available_actions.iter().any(|bound_action| {
+ // actions match...
+ bound_action.partial_eq(action)
+ }),
+ "On {} Failed to find {}",
+ line,
+ action.name(),
+ );
+ assert!(
+ // and key strokes contain the given key
+ bindings
+ .into_iter()
+ .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
+ "On {} Failed to find {} with key binding {}",
+ line,
+ action.name(),
+ key
+ );
+ }
+ }
+}