Detailed changes
@@ -1416,6 +1416,7 @@ dependencies = [
"settings",
"smol",
"sum_tree",
+ "sysinfo",
"tempfile",
"text",
"thiserror",
@@ -2791,7 +2792,6 @@ dependencies = [
"lazy_static",
"libc",
"log",
- "lsp",
"parking_lot 0.11.2",
"regex",
"rope",
@@ -7404,6 +7404,8 @@ dependencies = [
"anyhow",
"chrono",
"clap 4.4.4",
+ "fs",
+ "futures 0.3.28",
"gpui2",
"itertools 0.11.0",
"log",
@@ -7607,9 +7609,9 @@ dependencies = [
[[package]]
name = "sysinfo"
-version = "0.27.8"
+version = "0.29.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33"
+checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys 0.8.3",
@@ -8624,6 +8626,7 @@ dependencies = [
"anyhow",
"chrono",
"gpui2",
+ "rand 0.8.5",
"serde",
"settings",
"smallvec",
@@ -111,6 +111,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
+sysinfo = "0.29.10"
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
@@ -1,4 +1,4 @@
-web: cd ../zed.dev && PORT=3000 npx vercel dev
-collab: cd crates/collab && cargo run serve
+web: cd ../zed.dev && PORT=3000 npm run dev
+collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf
@@ -361,7 +361,7 @@
".venv",
"venv"
],
- // Can also be 'csh' and 'fish'
+ // Can also be 'csh', 'fish', and `nushell`
"activate_script": "default"
}
}
@@ -379,24 +379,24 @@
},
// Settings specific to our elixir integration
"elixir": {
- // Set Zed to use the experimental Next LS LSP server.
+ // Change the LSP zed uses for elixir.
// Note that changing this setting requires a restart of Zed
// to take effect.
//
// May take 3 values:
- // 1. Use the standard elixir-ls LSP server
- // "next": "off"
- // 2. Use a bundled version of the next Next LS LSP server
- // "next": "on",
- // 3. Use a local build of the next Next LS LSP server:
- // "next": {
+ // 1. Use the standard ElixirLS, this is the default
+ // "lsp": "elixir_ls"
+ // 2. Use the experimental NextLs
+ // "lsp": "next_ls",
+ // 3. Use a language server installed locally on your machine:
+ // "lsp": {
// "local": {
// "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"]
// }
// },
//
- "next": "off"
+ "lsp": "elixir_ls"
},
// Different settings for specific languages.
"languages": {
@@ -1,6 +1,6 @@
use crate::codegen::CodegenKind;
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
-use std::cmp;
+use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::iter;
use std::ops::Range;
@@ -14,59 +14,58 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
}
let selected_range = selected_range.to_offset(buffer);
- let mut matches = buffer.matches(0..buffer.len(), |grammar| {
+ let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
Some(&grammar.embedding_config.as_ref()?.query)
});
- let configs = matches
+ let configs = ts_matches
.grammars()
.iter()
.map(|g| g.embedding_config.as_ref().unwrap())
.collect::<Vec<_>>();
- let mut matches = iter::from_fn(move || {
- while let Some(mat) = matches.peek() {
- let config = &configs[mat.grammar_index];
- if let Some(collapse) = mat.captures.iter().find_map(|cap| {
- if Some(cap.index) == config.collapse_capture_ix {
- Some(cap.node.byte_range())
+ let mut matches = Vec::new();
+ while let Some(mat) = ts_matches.peek() {
+ let config = &configs[mat.grammar_index];
+ if let Some(collapse) = mat.captures.iter().find_map(|cap| {
+ if Some(cap.index) == config.collapse_capture_ix {
+ Some(cap.node.byte_range())
+ } else {
+ None
+ }
+ }) {
+ let mut keep = Vec::new();
+ for capture in mat.captures.iter() {
+ if Some(capture.index) == config.keep_capture_ix {
+ keep.push(capture.node.byte_range());
} else {
- None
- }
- }) {
- let mut keep = Vec::new();
- for capture in mat.captures.iter() {
- if Some(capture.index) == config.keep_capture_ix {
- keep.push(capture.node.byte_range());
- } else {
- continue;
- }
+ continue;
}
- matches.advance();
- return Some(Match { collapse, keep });
- } else {
- matches.advance();
}
+ ts_matches.advance();
+ matches.push(Match { collapse, keep });
+ } else {
+ ts_matches.advance();
}
- None
- })
- .peekable();
+ }
+ matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
+ let mut matches = matches.into_iter().peekable();
let mut summary = String::new();
let mut offset = 0;
let mut flushed_selection = false;
- while let Some(mut mat) = matches.next() {
+ while let Some(mat) = matches.next() {
// Keep extending the collapsed range if the next match surrounds
// the current one.
while let Some(next_mat) = matches.peek() {
- if next_mat.collapse.start <= mat.collapse.start
- && next_mat.collapse.end >= mat.collapse.end
+ if mat.collapse.start <= next_mat.collapse.start
+ && mat.collapse.end >= next_mat.collapse.end
{
- mat = matches.next().unwrap();
+ matches.next().unwrap();
} else {
break;
}
}
- if offset >= mat.collapse.start {
+ if offset > mat.collapse.start {
// Skip collapsed nodes that have already been summarized.
offset = cmp::max(offset, mat.collapse.end);
continue;
@@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
- let server_url = &auto_updater.read(cx).server_url;
+ let auto_updater = auto_updater.read(cx);
+ let server_url = &auto_updater.server_url;
+ let current_version = auto_updater.current_version;
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
- format!("{server_url}/releases/preview/latest")
+ format!("{server_url}/releases/preview/{current_version}")
} else {
- format!("{server_url}/releases/stable/latest")
+ format!("{server_url}/releases/stable/{current_version}")
};
cx.platform().open_url(&latest_release_url);
}
@@ -2,22 +2,23 @@ pub mod call_settings;
pub mod participant;
pub mod room;
-use std::sync::Arc;
-
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use channel::ChannelId;
-use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
+use client::{
+ proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+ ZED_ALWAYS_ACTIVE,
+};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
-use postage::watch;
-
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle,
};
+use postage::watch;
use project::Project;
+use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
@@ -68,6 +69,7 @@ impl ActiveCall {
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
+
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@@ -348,17 +350,22 @@ impl ActiveCall {
}
}
+ pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
+ self.location.as_ref()
+ }
+
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
- self.location = project.map(|project| project.downgrade());
- if let Some((room, _)) = self.room.as_ref() {
- room.update(cx, |room, cx| room.set_location(project, cx))
- } else {
- Task::ready(Ok(()))
+ if project.is_some() || !*ZED_ALWAYS_ACTIVE {
+ self.location = project.map(|project| project.downgrade());
+ if let Some((room, _)) = self.room.as_ref() {
+ return room.update(cx, |room, cx| room.set_location(project, cx));
+ }
}
+ Task::ready(Ok(()))
}
fn set_room(
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
+use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModelHandle;
@@ -43,6 +44,7 @@ pub struct RemoteParticipant {
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
+ pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
use audio::{Audio, Sound};
use client::{
proto::{self, PeerId},
- Client, TypedEnvelope, User, UserStore,
+ Client, ParticipantIndex, TypedEnvelope, User, UserStore,
};
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
@@ -44,6 +44,12 @@ pub enum Event {
RemoteProjectUnshared {
project_id: u64,
},
+ RemoteProjectJoined {
+ project_id: u64,
+ },
+ RemoteProjectInvitationDiscarded {
+ project_id: u64,
+ },
Left,
}
@@ -714,6 +720,9 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
+ participant_index: ParticipantIndex(
+ participant.participant_index,
+ ),
peer_id,
projects: participant.projects,
location,
@@ -807,6 +816,15 @@ impl Room {
let _ = this.leave(cx);
}
+ this.user_store.update(cx, |user_store, cx| {
+ let participant_indices_by_user_id = this
+ .remote_participants
+ .iter()
+ .map(|(user_id, participant)| (*user_id, participant.participant_index))
+ .collect();
+ user_store.set_participant_indices(participant_indices_by_user_id, cx);
+ });
+
this.check_invariants();
cx.notify();
});
@@ -1003,6 +1021,7 @@ impl Room {
) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
+ cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
@@ -1,22 +1,25 @@
use crate::Channel;
use anyhow::Result;
-use client::Client;
+use client::{Client, Collaborator, UserStore};
+use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
-use rpc::{proto, TypedEnvelope};
+use rpc::{
+ proto::{self, PeerId},
+ TypedEnvelope,
+};
use std::sync::Arc;
use util::ResultExt;
pub(crate) fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
- client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
- client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
- client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
+ client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
}
pub struct ChannelBuffer {
pub(crate) channel: Arc<Channel>,
connected: bool,
- collaborators: Vec<proto::Collaborator>,
+ collaborators: HashMap<PeerId, Collaborator>,
+ user_store: ModelHandle<UserStore>,
buffer: ModelHandle<language::Buffer>,
buffer_epoch: u64,
client: Arc<Client>,
@@ -46,6 +49,7 @@ impl ChannelBuffer {
pub(crate) async fn new(
channel: Arc<Channel>,
client: Arc<Client>,
+ user_store: ModelHandle<UserStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
let response = client
@@ -61,8 +65,6 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
- let collaborators = response.collaborators;
-
let buffer = cx.add_model(|_| {
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
});
@@ -73,34 +75,45 @@ impl ChannelBuffer {
anyhow::Ok(cx.add_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
- Self {
+ let mut this = Self {
buffer,
buffer_epoch: response.epoch,
client,
connected: true,
- collaborators,
+ collaborators: Default::default(),
channel,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
- }
+ user_store,
+ };
+ this.replace_collaborators(response.collaborators, cx);
+ this
}))
}
+ pub fn user_store(&self) -> &ModelHandle<UserStore> {
+ &self.user_store
+ }
+
pub(crate) fn replace_collaborators(
&mut self,
collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>,
) {
- for old_collaborator in &self.collaborators {
- if collaborators
- .iter()
- .any(|c| c.replica_id == old_collaborator.replica_id)
- {
+ let mut new_collaborators = HashMap::default();
+ for collaborator in collaborators {
+ if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
+ new_collaborators.insert(collaborator.peer_id, collaborator);
+ }
+ }
+
+ for (_, old_collaborator) in &self.collaborators {
+ if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
});
}
}
- self.collaborators = collaborators;
+ self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
}
@@ -127,64 +140,14 @@ impl ChannelBuffer {
Ok(())
}
- async fn handle_add_channel_buffer_collaborator(
- this: ModelHandle<Self>,
- envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- let collaborator = envelope.payload.collaborator.ok_or_else(|| {
- anyhow::anyhow!(
- "Should have gotten a collaborator in the AddChannelBufferCollaborator message"
- )
- })?;
-
- this.update(&mut cx, |this, cx| {
- this.collaborators.push(collaborator);
- cx.emit(ChannelBufferEvent::CollaboratorsChanged);
- cx.notify();
- });
-
- Ok(())
- }
-
- async fn handle_remove_channel_buffer_collaborator(
+ async fn handle_update_channel_buffer_collaborators(
this: ModelHandle<Self>,
- message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+ message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
- this.collaborators.retain(|collaborator| {
- if collaborator.peer_id == message.payload.peer_id {
- this.buffer.update(cx, |buffer, cx| {
- buffer.remove_peer(collaborator.replica_id as u16, cx)
- });
- false
- } else {
- true
- }
- });
- cx.emit(ChannelBufferEvent::CollaboratorsChanged);
- cx.notify();
- });
-
- Ok(())
- }
-
- async fn handle_update_channel_buffer_collaborator(
- this: ModelHandle<Self>,
- message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- for collaborator in &mut this.collaborators {
- if collaborator.peer_id == message.payload.old_peer_id {
- collaborator.peer_id = message.payload.new_peer_id;
- break;
- }
- }
+ this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
@@ -217,7 +180,7 @@ impl ChannelBuffer {
self.buffer.clone()
}
- pub fn collaborators(&self) -> &[proto::Collaborator] {
+ pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}
@@ -198,10 +198,11 @@ impl ChannelStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
let client = self.client.clone();
+ let user_store = self.user_store.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_buffers,
- |channel, cx| ChannelBuffer::new(channel, client, cx),
+ |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
cx,
)
}
@@ -33,15 +33,16 @@ parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
smol.workspace = true
+sysinfo.workspace = true
+tempfile = "3"
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
-uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2"
-serde.workspace = true
-serde_derive.workspace = true
-tempfile = "3"
+uuid = { version = "1.1.2", features = ["v4"] }
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
@@ -62,6 +62,8 @@ lazy_static! {
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
+ pub static ref ZED_ALWAYS_ACTIVE: bool =
+ std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -4,6 +4,7 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
+use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
@@ -88,6 +89,16 @@ pub enum ClickhouseEvent {
kind: AssistantKind,
model: &'static str,
},
+ Cpu {
+ usage_as_percent: f32,
+ core_count: u32,
+ },
+ Memory {
+ memory_in_bytes: u64,
+ virtual_memory_in_bytes: u64,
+ start_time_in_seconds: u64,
+ run_time_in_seconds: u64,
+ },
}
#[cfg(debug_assertions)]
@@ -136,7 +147,7 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
- pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
+ pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
@@ -145,6 +156,48 @@ impl Telemetry {
if has_clickhouse_events {
self.flush_clickhouse_events();
}
+
+ let this = self.clone();
+ cx.spawn(|mut cx| async move {
+ let mut system = System::new_all();
+ system.refresh_all();
+
+ loop {
+ // Waiting some amount of time before the first query is important to get a reasonable value
+ // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
+ const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
+ smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
+
+ let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
+
+ system.refresh_memory();
+ system.refresh_processes();
+
+ let current_process = Pid::from_u32(std::process::id());
+ let Some(process) = system.processes().get(¤t_process) else {
+ let process = current_process;
+ log::error!("Failed to find own process {process:?} in system process table");
+ // TODO: Fire an error telemetry event
+ return;
+ };
+
+ let memory_event = ClickhouseEvent::Memory {
+ memory_in_bytes: process.memory(),
+ virtual_memory_in_bytes: process.virtual_memory(),
+ start_time_in_seconds: process.start_time(),
+ run_time_in_seconds: process.run_time(),
+ };
+
+ let cpu_event = ClickhouseEvent::Cpu {
+ usage_as_percent: process.cpu_usage(),
+ core_count: system.cpus().len() as u32,
+ };
+
+ this.report_clickhouse_event(memory_event, telemetry_settings);
+ this.report_clickhouse_event(cpu_event, telemetry_settings);
+ }
+ })
+ .detach();
}
pub fn set_authenticated_user_info(
@@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
+use text::ReplicaId;
use util::http::HttpClient;
use util::TryFutureExt as _;
pub type UserId = u64;
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ParticipantIndex(pub u32);
+
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
@@ -19,6 +23,13 @@ pub struct User {
pub avatar: Option<Arc<ImageData>>,
}
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Collaborator {
+ pub peer_id: proto::PeerId,
+ pub replica_id: ReplicaId,
+ pub user_id: UserId,
+}
+
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
@@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
+ participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>,
@@ -81,6 +93,7 @@ pub enum Event {
kind: ContactEventKind,
},
ShowContacts,
+ ParticipantIndicesChanged,
}
#[derive(Clone, Copy)]
@@ -118,6 +131,7 @@ impl UserStore {
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
+ participant_indices: Default::default(),
outgoing_contact_requests: Default::default(),
invite_info: None,
client: Arc::downgrade(&client),
@@ -581,6 +595,10 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
+ pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
+ self.users.get(&user_id).cloned()
+ }
+
pub fn get_user(
&mut self,
user_id: u64,
@@ -641,6 +659,21 @@ impl UserStore {
}
})
}
+
+ pub fn set_participant_indices(
+ &mut self,
+ participant_indices: HashMap<u64, ParticipantIndex>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if participant_indices != self.participant_indices {
+ self.participant_indices = participant_indices;
+ cx.emit(Event::ParticipantIndicesChanged);
+ }
+ }
+
+ pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
+ &self.participant_indices
+ }
}
impl User {
@@ -672,6 +705,16 @@ impl Contact {
}
}
+impl Collaborator {
+ pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
+ Ok(Self {
+ peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
+ replica_id: message.replica_id as ReplicaId,
+ user_id: message.user_id as UserId,
+ })
+ }
+}
+
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let mut response = http
.get(url, Default::default(), true)
@@ -158,7 +158,8 @@ CREATE TABLE "room_participants" (
"initial_project_id" INTEGER,
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
"calling_connection_id" INTEGER NOT NULL,
- "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
+ "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
+ "participant_index" INTEGER
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
@@ -0,0 +1 @@
+ALTER TABLE room_participants ADD COLUMN participant_index INTEGER;
@@ -510,7 +510,7 @@ pub struct RefreshedRoom {
pub struct RefreshedChannelBuffer {
pub connection_ids: Vec<ConnectionId>,
- pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
+ pub collaborators: Vec<proto::Collaborator>,
}
pub struct Project {
@@ -2,6 +2,12 @@ use super::*;
use prost::Message;
use text::{EditOperation, UndoOperation};
+pub struct LeftChannelBuffer {
+ pub channel_id: ChannelId,
+ pub collaborators: Vec<proto::Collaborator>,
+ pub connections: Vec<ConnectionId>,
+}
+
impl Database {
pub async fn join_channel_buffer(
&self,
@@ -204,23 +210,26 @@ impl Database {
server_id: ServerId,
) -> Result<RefreshedChannelBuffer> {
self.transaction(|tx| async move {
- let collaborators = channel_buffer_collaborator::Entity::find()
+ let db_collaborators = channel_buffer_collaborator::Entity::find()
.filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
.all(&*tx)
.await?;
let mut connection_ids = Vec::new();
- let mut removed_collaborators = Vec::new();
+ let mut collaborators = Vec::new();
let mut collaborator_ids_to_remove = Vec::new();
- for collaborator in &collaborators {
- if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
- connection_ids.push(collaborator.connection());
+ for db_collaborator in &db_collaborators {
+ if !db_collaborator.connection_lost
+ && db_collaborator.connection_server_id == server_id
+ {
+ connection_ids.push(db_collaborator.connection());
+ collaborators.push(proto::Collaborator {
+ peer_id: Some(db_collaborator.connection().into()),
+ replica_id: db_collaborator.replica_id.0 as u32,
+ user_id: db_collaborator.user_id.to_proto(),
+ })
} else {
- removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
- channel_id: channel_id.to_proto(),
- peer_id: Some(collaborator.connection().into()),
- });
- collaborator_ids_to_remove.push(collaborator.id);
+ collaborator_ids_to_remove.push(db_collaborator.id);
}
}
@@ -231,7 +240,7 @@ impl Database {
Ok(RefreshedChannelBuffer {
connection_ids,
- removed_collaborators,
+ collaborators,
})
})
.await
@@ -241,7 +250,7 @@ impl Database {
&self,
channel_id: ChannelId,
connection: ConnectionId,
- ) -> Result<Vec<ConnectionId>> {
+ ) -> Result<LeftChannelBuffer> {
self.transaction(|tx| async move {
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
.await
@@ -275,7 +284,7 @@ impl Database {
pub async fn leave_channel_buffers(
&self,
connection: ConnectionId,
- ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+ ) -> Result<Vec<LeftChannelBuffer>> {
self.transaction(|tx| async move {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryChannelIds {
@@ -294,10 +303,10 @@ impl Database {
let mut result = Vec::new();
for channel_id in channel_ids {
- let collaborators = self
+ let left_channel_buffer = self
.leave_channel_buffer_internal(channel_id, connection, &*tx)
.await?;
- result.push((channel_id, collaborators));
+ result.push(left_channel_buffer);
}
Ok(result)
@@ -310,7 +319,7 @@ impl Database {
channel_id: ChannelId,
connection: ConnectionId,
tx: &DatabaseTransaction,
- ) -> Result<Vec<ConnectionId>> {
+ ) -> Result<LeftChannelBuffer> {
let result = channel_buffer_collaborator::Entity::delete_many()
.filter(
Condition::all()
@@ -327,6 +336,7 @@ impl Database {
Err(anyhow!("not a collaborator on this project"))?;
}
+ let mut collaborators = Vec::new();
let mut connections = Vec::new();
let mut rows = channel_buffer_collaborator::Entity::find()
.filter(
@@ -336,19 +346,26 @@ impl Database {
.await?;
while let Some(row) = rows.next().await {
let row = row?;
- connections.push(ConnectionId {
- id: row.connection_id as u32,
- owner_id: row.connection_server_id.0 as u32,
+ let connection = row.connection();
+ connections.push(connection);
+ collaborators.push(proto::Collaborator {
+ peer_id: Some(connection.into()),
+ replica_id: row.replica_id.0 as u32,
+ user_id: row.user_id.to_proto(),
});
}
drop(rows);
- if connections.is_empty() {
+ if collaborators.is_empty() {
self.snapshot_channel_buffer(channel_id, &tx).await?;
}
- Ok(connections)
+ Ok(LeftChannelBuffer {
+ channel_id,
+ collaborators,
+ connections,
+ })
}
pub async fn get_channel_buffer_collaborators(
@@ -738,7 +738,7 @@ impl Database {
Condition::any()
.add(
Condition::all()
- .add(follower::Column::ProjectId.eq(project_id))
+ .add(follower::Column::ProjectId.eq(Some(project_id)))
.add(
follower::Column::LeaderConnectionServerId
.eq(connection.owner_id),
@@ -747,7 +747,7 @@ impl Database {
)
.add(
Condition::all()
- .add(follower::Column::ProjectId.eq(project_id))
+ .add(follower::Column::ProjectId.eq(Some(project_id)))
.add(
follower::Column::FollowerConnectionServerId
.eq(connection.owner_id),
@@ -862,13 +862,46 @@ impl Database {
.await
}
+ pub async fn check_room_participants(
+ &self,
+ room_id: RoomId,
+ leader_id: ConnectionId,
+ follower_id: ConnectionId,
+ ) -> Result<()> {
+ self.transaction(|tx| async move {
+ use room_participant::Column;
+
+ let count = room_participant::Entity::find()
+ .filter(
+ Condition::all().add(Column::RoomId.eq(room_id)).add(
+ Condition::any()
+ .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
+ Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
+ ))
+ .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
+ Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
+ )),
+ ),
+ )
+ .count(&*tx)
+ .await?;
+
+ if count < 2 {
+ Err(anyhow!("not room participants"))?;
+ }
+
+ Ok(())
+ })
+ .await
+ }
+
pub async fn follow(
&self,
+ room_id: RoomId,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
- let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
@@ -894,15 +927,16 @@ impl Database {
pub async fn unfollow(
&self,
+ room_id: RoomId,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
- let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many()
.filter(
Condition::all()
+ .add(follower::Column::RoomId.eq(room_id))
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
@@ -128,6 +128,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
+ participant_index: ActiveValue::set(Some(0)),
..Default::default()
}
.insert(&*tx)
@@ -152,6 +153,7 @@ impl Database {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(called_user_id),
answering_connection_lost: ActiveValue::set(false),
+ participant_index: ActiveValue::NotSet,
calling_user_id: ActiveValue::set(calling_user_id),
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -283,6 +285,26 @@ impl Database {
.await?
.ok_or_else(|| anyhow!("no such room"))?;
+ #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+ enum QueryParticipantIndices {
+ ParticipantIndex,
+ }
+ let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
+ .filter(
+ room_participant::Column::RoomId
+ .eq(room_id)
+ .and(room_participant::Column::ParticipantIndex.is_not_null()),
+ )
+ .select_only()
+ .column(room_participant::Column::ParticipantIndex)
+ .into_values::<_, QueryParticipantIndices>()
+ .all(&*tx)
+ .await?;
+ let mut participant_index = 0;
+ while existing_participant_indices.contains(&participant_index) {
+ participant_index += 1;
+ }
+
if let Some(channel_id) = channel_id {
self.check_user_is_channel_member(channel_id, user_id, &*tx)
.await?;
@@ -300,6 +322,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
+ participant_index: ActiveValue::Set(Some(participant_index)),
..Default::default()
}])
.on_conflict(
@@ -308,6 +331,7 @@ impl Database {
room_participant::Column::AnsweringConnectionId,
room_participant::Column::AnsweringConnectionServerId,
room_participant::Column::AnsweringConnectionLost,
+ room_participant::Column::ParticipantIndex,
])
.to_owned(),
)
@@ -322,6 +346,7 @@ impl Database {
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.set(room_participant::ActiveModel {
+ participant_index: ActiveValue::Set(Some(participant_index)),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
@@ -960,6 +985,39 @@ impl Database {
Ok(room)
}
+ pub async fn room_connection_ids(
+ &self,
+ room_id: RoomId,
+ connection_id: ConnectionId,
+ ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
+ self.room_transaction(room_id, |tx| async move {
+ let mut participants = room_participant::Entity::find()
+ .filter(room_participant::Column::RoomId.eq(room_id))
+ .stream(&*tx)
+ .await?;
+
+ let mut is_participant = false;
+ let mut connection_ids = HashSet::default();
+ while let Some(participant) = participants.next().await {
+ let participant = participant?;
+ if let Some(answering_connection) = participant.answering_connection() {
+ if answering_connection == connection_id {
+ is_participant = true;
+ } else {
+ connection_ids.insert(answering_connection);
+ }
+ }
+ }
+
+ if !is_participant {
+ Err(anyhow!("not a room participant"))?;
+ }
+
+ Ok(connection_ids)
+ })
+ .await
+ }
+
async fn get_channel_room(
&self,
room_id: RoomId,
@@ -978,10 +1036,15 @@ impl Database {
let mut pending_participants = Vec::new();
while let Some(db_participant) = db_participants.next().await {
let db_participant = db_participant?;
- if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
- .answering_connection_id
- .zip(db_participant.answering_connection_server_id)
- {
+ if let (
+ Some(answering_connection_id),
+ Some(answering_connection_server_id),
+ Some(participant_index),
+ ) = (
+ db_participant.answering_connection_id,
+ db_participant.answering_connection_server_id,
+ db_participant.participant_index,
+ ) {
let location = match (
db_participant.location_kind,
db_participant.location_project_id,
@@ -1012,6 +1075,7 @@ impl Database {
peer_id: Some(answering_connection.into()),
projects: Default::default(),
location: Some(proto::ParticipantLocation { variant: location }),
+ participant_index: participant_index as u32,
},
);
} else {
@@ -1,4 +1,5 @@
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
+use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -17,6 +18,16 @@ pub struct Model {
pub calling_user_id: UserId,
pub calling_connection_id: i32,
pub calling_connection_server_id: Option<ServerId>,
+ pub participant_index: Option<i32>,
+}
+
+impl Model {
+ pub fn answering_connection(&self) -> Option<ConnectionId> {
+ Some(ConnectionId {
+ owner_id: self.answering_connection_server_id?.0 as u32,
+ id: self.answering_connection_id? as u32,
+ })
+ }
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
assert_eq!(zed_collaborats, &[a_id, b_id]);
- let collaborators = db
+ let left_buffer = db
.leave_channel_buffer(zed_id, connection_id_b)
.await
.unwrap();
- assert_eq!(collaborators, &[connection_id_a],);
+ assert_eq!(left_buffer.connections, &[connection_id_a],);
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
let _ = db
@@ -38,8 +38,8 @@ use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge};
use rpc::{
proto::{
- self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
- EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
+ self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
+ LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
};
@@ -313,9 +313,16 @@ impl Server {
.trace_err()
{
for connection_id in refreshed_channel_buffer.connection_ids {
- for message in &refreshed_channel_buffer.removed_collaborators {
- peer.send(connection_id, message.clone()).trace_err();
- }
+ peer.send(
+ connection_id,
+ proto::UpdateChannelBufferCollaborators {
+ channel_id: channel_id.to_proto(),
+ collaborators: refreshed_channel_buffer
+ .collaborators
+ .clone(),
+ },
+ )
+ .trace_err();
}
}
}
@@ -1883,24 +1890,19 @@ async fn follow(
response: Response<proto::Follow>,
session: Session,
) -> Result<()> {
- let project_id = ProjectId::from_proto(request.project_id);
+ let room_id = RoomId::from_proto(request.room_id);
+ let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
- {
- let project_connection_ids = session
- .db()
- .await
- .project_connection_ids(project_id, session.connection_id)
- .await?;
-
- if !project_connection_ids.contains(&leader_id) {
- Err(anyhow!("no such peer"))?;
- }
- }
+ session
+ .db()
+ .await
+ .check_room_participants(room_id, leader_id, session.connection_id)
+ .await?;
let mut response_payload = session
.peer
@@ -1911,56 +1913,63 @@ async fn follow(
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
- let room = session
- .db()
- .await
- .follow(project_id, leader_id, follower_id)
- .await?;
- room_updated(&room, &session.peer);
+ if let Some(project_id) = project_id {
+ let room = session
+ .db()
+ .await
+ .follow(room_id, project_id, leader_id, follower_id)
+ .await?;
+ room_updated(&room, &session.peer);
+ }
Ok(())
}
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
- let project_id = ProjectId::from_proto(request.project_id);
+ let room_id = RoomId::from_proto(request.room_id);
+ let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
- if !session
+ session
.db()
.await
- .project_connection_ids(project_id, session.connection_id)
- .await?
- .contains(&leader_id)
- {
- Err(anyhow!("no such peer"))?;
- }
+ .check_room_participants(room_id, leader_id, session.connection_id)
+ .await?;
session
.peer
.forward_send(session.connection_id, leader_id, request)?;
- let room = session
- .db()
- .await
- .unfollow(project_id, leader_id, follower_id)
- .await?;
- room_updated(&room, &session.peer);
+ if let Some(project_id) = project_id {
+ let room = session
+ .db()
+ .await
+ .unfollow(room_id, project_id, leader_id, follower_id)
+ .await?;
+ room_updated(&room, &session.peer);
+ }
Ok(())
}
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
- let project_id = ProjectId::from_proto(request.project_id);
- let project_connection_ids = session
- .db
- .lock()
- .await
- .project_connection_ids(project_id, session.connection_id)
- .await?;
+ let room_id = RoomId::from_proto(request.room_id);
+ let database = session.db.lock().await;
+
+ let connection_ids = if let Some(project_id) = request.project_id {
+ let project_id = ProjectId::from_proto(project_id);
+ database
+ .project_connection_ids(project_id, session.connection_id)
+ .await?
+ } else {
+ database
+ .room_connection_ids(room_id, session.connection_id)
+ .await?
+ };
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
@@ -1969,9 +1978,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
});
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
- if project_connection_ids.contains(&follower_connection_id)
- && Some(follower_peer_id) != leader_id
- {
+ if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
session.peer.forward_send(
session.connection_id,
follower_connection_id,
@@ -2658,18 +2665,12 @@ async fn join_channel_buffer(
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
.await?;
- let replica_id = open_response.replica_id;
let collaborators = open_response.collaborators.clone();
-
response.send(open_response)?;
- let update = AddChannelBufferCollaborator {
+ let update = UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
- collaborator: Some(proto::Collaborator {
- user_id: session.user_id.to_proto(),
- peer_id: Some(session.connection_id.into()),
- replica_id,
- }),
+ collaborators: collaborators.clone(),
};
channel_buffer_updated(
session.connection_id,
@@ -2716,8 +2717,8 @@ async fn rejoin_channel_buffers(
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
.await?;
- for buffer in &buffers {
- let collaborators_to_notify = buffer
+ for rejoined_buffer in &buffers {
+ let collaborators_to_notify = rejoined_buffer
.buffer
.collaborators
.iter()
@@ -2725,10 +2726,9 @@ async fn rejoin_channel_buffers(
channel_buffer_updated(
session.connection_id,
collaborators_to_notify,
- &proto::UpdateChannelBufferCollaborator {
- channel_id: buffer.buffer.channel_id,
- old_peer_id: Some(buffer.old_connection_id.into()),
- new_peer_id: Some(session.connection_id.into()),
+ &proto::UpdateChannelBufferCollaborators {
+ channel_id: rejoined_buffer.buffer.channel_id,
+ collaborators: rejoined_buffer.buffer.collaborators.clone(),
},
&session.peer,
);
@@ -2749,7 +2749,7 @@ async fn leave_channel_buffer(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
- let collaborators_to_notify = db
+ let left_buffer = db
.leave_channel_buffer(channel_id, session.connection_id)
.await?;
@@ -2757,10 +2757,10 @@ async fn leave_channel_buffer(
channel_buffer_updated(
session.connection_id,
- collaborators_to_notify,
- &proto::RemoveChannelBufferCollaborator {
+ left_buffer.connections,
+ &proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
- peer_id: Some(session.connection_id.into()),
+ collaborators: left_buffer.collaborators,
},
&session.peer,
);
@@ -3235,13 +3235,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
.leave_channel_buffers(session.connection_id)
.await?;
- for (channel_id, connections) in left_channel_buffers {
+ for left_buffer in left_channel_buffers {
channel_buffer_updated(
session.connection_id,
- connections,
- &proto::RemoveChannelBufferCollaborator {
- channel_id: channel_id.to_proto(),
- peer_id: Some(session.connection_id.into()),
+ left_buffer.connections,
+ &proto::UpdateChannelBufferCollaborators {
+ channel_id: left_buffer.channel_id.to_proto(),
+ collaborators: left_buffer.collaborators,
},
&session.peer,
);
@@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
mod channel_buffer_tests;
mod channel_message_tests;
mod channel_tests;
+mod following_tests;
mod integration_tests;
mod random_channel_buffer_tests;
mod random_project_collaboration_tests;
@@ -4,14 +4,16 @@ use crate::{
};
use call::ActiveCall;
use channel::Channel;
-use client::UserId;
+use client::ParticipantIndex;
+use client::{Collaborator, UserId};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
+use editor::{Anchor, Editor, ToOffset};
use futures::future;
-use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
-use rpc::{proto, RECEIVE_TIMEOUT};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
+use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use serde_json::json;
-use std::sync::Arc;
+use std::{ops::Range, sync::Arc};
#[gpui::test]
async fn test_core_channel_buffers(
@@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(
&buffer.collaborators(),
- &[client_b.user_id(), client_a.user_id()],
+ &[client_a.user_id(), client_b.user_id()],
);
});
@@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
}
#[gpui::test]
-async fn test_channel_buffer_replica_ids(
+async fn test_channel_notes_participant_indices(
deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
+ mut cx_a: &mut TestAppContext,
+ mut cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
@@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids(
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").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);
+ cx_c.update(editor::init);
+
let channel_id = server
.make_channel(
"the-channel",
@@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
)
.await;
- let active_call_a = cx_a.read(ActiveCall::global);
- let active_call_b = cx_b.read(ActiveCall::global);
- let active_call_c = cx_c.read(ActiveCall::global);
-
- // Clients A and B join a channel.
- active_call_a
- .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
- .await
- .unwrap();
- active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
- .await
- .unwrap();
-
- // Clients A, B, and C join a channel buffer
- // C first so that the replica IDs in the project and the channel buffer are different
- let channel_buffer_c = client_c
- .channel_store()
- .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
+ client_a
+ .fs()
+ .insert_tree("/root", json!({"file.txt": "123"}))
+ .await;
+ let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
+ let project_b = client_b.build_empty_local_project(cx_b);
+ let project_c = client_c.build_empty_local_project(cx_c);
+ let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+ let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+ // Clients A, B, and C open the channel notes
+ let channel_view_a = cx_a
+ .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
.await
.unwrap();
- let channel_buffer_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+ let channel_view_b = cx_b
+ .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.await
.unwrap();
- let channel_buffer_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
- .await
- .unwrap();
-
- // Client B shares a project
- client_b
- .fs()
- .insert_tree("/dir", json!({ "file.txt": "contents" }))
- .await;
- let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
- let shared_project_id = active_call_b
- .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+ let channel_view_c = cx_c
+ .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
.await
.unwrap();
- // Client A joins the project
- let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
- deterministic.run_until_parked();
-
- // Client C is in a separate project.
- client_c.fs().insert_tree("/dir", json!({})).await;
- let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
-
- // Note that each user has a different replica id in the projects vs the
- // channel buffer.
- channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
- assert_eq!(project_a.read(cx).replica_id(), 1);
- assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
+ // Clients A, B, and C all insert and select some text
+ channel_view_a.update(cx_a, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ editor.insert("a", cx);
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![0..1]);
+ });
+ });
});
- channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
- assert_eq!(project_b.read(cx).replica_id(), 0);
- assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
+ deterministic.run_until_parked();
+ channel_view_b.update(cx_b, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ editor.move_down(&Default::default(), cx);
+ editor.insert("b", cx);
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![1..2]);
+ });
+ });
});
- channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
- // C is not in the project
- assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
+ deterministic.run_until_parked();
+ channel_view_c.update(cx_c, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ editor.move_down(&Default::default(), cx);
+ editor.insert("c", cx);
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![2..3]);
+ });
+ });
});
- let channel_window_a =
- cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
- let channel_window_b =
- cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
- let channel_window_c = cx_c.add_window(|cx| {
- ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
+ // Client A sees clients B and C without assigned colors, because they aren't
+ // in a call together.
+ deterministic.run_until_parked();
+ channel_view_a.update(cx_a, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
+ });
});
- let channel_view_a = channel_window_a.root(cx_a);
- let channel_view_b = channel_window_b.root(cx_b);
- let channel_view_c = channel_window_c.root(cx_c);
+ // Clients A and B join the same call.
+ for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
+ call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
+ .await
+ .unwrap();
+ }
- // For clients A and B, the replica ids in the channel buffer are mapped
- // so that they match the same users' replica ids in their shared project.
- channel_view_a.read_with(cx_a, |view, cx| {
- assert_eq!(
- view.editor.read(cx).replica_id_map().unwrap(),
- &[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
- );
+ // Clients A and B see each other with two different assigned colors. Client C
+ // still doesn't have a color.
+ deterministic.run_until_parked();
+ channel_view_a.update(cx_a, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ assert_remote_selections(
+ editor,
+ &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
+ cx,
+ );
+ });
});
- channel_view_b.read_with(cx_b, |view, cx| {
- assert_eq!(
- view.editor.read(cx).replica_id_map().unwrap(),
- &[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
- )
+ channel_view_b.update(cx_b, |notes, cx| {
+ notes.editor.update(cx, |editor, cx| {
+ assert_remote_selections(
+ editor,
+ &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
+ cx,
+ );
+ });
});
- // Client C only sees themself, as they're not part of any shared project
- channel_view_c.read_with(cx_c, |view, cx| {
- assert_eq!(
- view.editor.read(cx).replica_id_map().unwrap(),
- &[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
- );
- });
+ // Client A shares a project, and client B joins.
+ 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);
- // Client C joins the project that clients A and B are in.
- active_call_c
- .update(cx_c, |call, cx| call.join_channel(channel_id, cx))
+ // Clients A and B open the same file.
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+ })
.await
+ .unwrap()
+ .downcast::<Editor>()
.unwrap();
- let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
- deterministic.run_until_parked();
- project_c.read_with(cx_c, |project, _| {
- assert_eq!(project.replica_id(), 2);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ editor_a.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![0..1]);
+ });
});
+ editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![2..3]);
+ });
+ });
+ deterministic.run_until_parked();
- // For clients A and B, client C's replica id in the channel buffer is
- // now mapped to their replica id in the shared project.
- channel_view_a.read_with(cx_a, |view, cx| {
- assert_eq!(
- view.editor.read(cx).replica_id_map().unwrap(),
- &[(1, 0), (2, 1), (0, 2)]
- .into_iter()
- .collect::<HashMap<_, _>>()
- );
+ // Clients A and B see each other with the same colors as in the channel notes.
+ editor_a.update(cx_a, |editor, cx| {
+ assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
});
- channel_view_b.read_with(cx_b, |view, cx| {
- assert_eq!(
- view.editor.read(cx).replica_id_map().unwrap(),
- &[(1, 0), (2, 1), (0, 2)]
- .into_iter()
- .collect::<HashMap<_, _>>(),
- )
+ editor_b.update(cx_b, |editor, cx| {
+ assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
});
}
+#[track_caller]
+fn assert_remote_selections(
+ editor: &mut Editor,
+ expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
+ cx: &mut ViewContext<Editor>,
+) {
+ let snapshot = editor.snapshot(cx);
+ let range = Anchor::min()..Anchor::max();
+ let remote_selections = snapshot
+ .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
+ .map(|s| {
+ let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
+ let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
+ (s.participant_index, start..end)
+ })
+ .collect::<Vec<_>>();
+ assert_eq!(
+ remote_selections, expected_selections,
+ "incorrect remote selections"
+ );
+}
+
#[gpui::test]
-async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+async fn test_multiple_handles_to_channel_buffer(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -565,26 +607,163 @@ async fn test_channel_buffers_and_server_restarts(
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
- assert_eq!(
- buffer_a
- .collaborators()
- .iter()
- .map(|c| c.user_id)
- .collect::<Vec<_>>(),
- vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
+ assert_collaborators(
+ buffer_a.collaborators(),
+ &[client_a.user_id(), client_b.user_id()],
);
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
});
});
}
+#[gpui::test(iterations = 10)]
+async fn test_following_to_channel_notes_without_a_shared_project(
+ deterministic: Arc<Deterministic>,
+ mut cx_a: &mut TestAppContext,
+ mut cx_b: &mut TestAppContext,
+ mut cx_c: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+ cx_c.update(editor::init);
+ cx_a.update(collab_ui::channel_view::init);
+ cx_b.update(collab_ui::channel_view::init);
+ cx_c.update(collab_ui::channel_view::init);
+
+ let channel_1_id = server
+ .make_channel(
+ "channel-1",
+ None,
+ (&client_a, cx_a),
+ &mut [(&client_b, cx_b), (&client_c, cx_c)],
+ )
+ .await;
+ let channel_2_id = server
+ .make_channel(
+ "channel-2",
+ None,
+ (&client_a, cx_a),
+ &mut [(&client_b, cx_b), (&client_c, cx_c)],
+ )
+ .await;
+
+ // Clients A, B, and C join a channel.
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+ let active_call_c = cx_c.read(ActiveCall::global);
+ for (call, cx) in [
+ (&active_call_a, &mut cx_a),
+ (&active_call_b, &mut cx_b),
+ (&active_call_c, &mut cx_c),
+ ] {
+ call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
+ .await
+ .unwrap();
+ }
+ deterministic.run_until_parked();
+
+ // Clients A, B, and C all open their own unshared projects.
+ client_a.fs().insert_tree("/a", json!({})).await;
+ client_b.fs().insert_tree("/b", json!({})).await;
+ client_c.fs().insert_tree("/c", json!({})).await;
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+ let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
+ let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
+ let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+ let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ // Client A opens the notes for channel 1.
+ let channel_view_1_a = cx_a
+ .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
+ .await
+ .unwrap();
+ channel_view_1_a.update(cx_a, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-1");
+ notes.editor.update(cx, |editor, cx| {
+ editor.insert("Hello from A.", cx);
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![3..4]);
+ });
+ });
+ });
+
+ // Client B follows client A.
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ // Client B is taken to the notes for channel 1, with the same
+ // text selected as client A.
+ deterministic.run_until_parked();
+ let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_a.peer_id().unwrap())
+ );
+ workspace
+ .active_item(cx)
+ .expect("no active item")
+ .downcast::<ChannelView>()
+ .expect("active item is not a channel view")
+ });
+ channel_view_1_b.read_with(cx_b, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-1");
+ let editor = notes.editor.read(cx);
+ assert_eq!(editor.text(cx), "Hello from A.");
+ assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
+ });
+
+ // Client A opens the notes for channel 2.
+ let channel_view_2_a = cx_a
+ .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
+ .await
+ .unwrap();
+ channel_view_2_a.read_with(cx_a, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-2");
+ });
+
+ // Client B is taken to the notes for channel 2.
+ deterministic.run_until_parked();
+ let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_a.peer_id().unwrap())
+ );
+ workspace
+ .active_item(cx)
+ .expect("no active item")
+ .downcast::<ChannelView>()
+ .expect("active item is not a channel view")
+ });
+ channel_view_2_b.read_with(cx_b, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-2");
+ });
+}
+
#[track_caller]
-fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
+fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
+ let mut user_ids = collaborators
+ .values()
+ .map(|collaborator| collaborator.user_id)
+ .collect::<Vec<_>>();
+ user_ids.sort();
assert_eq!(
- collaborators
- .into_iter()
- .map(|collaborator| collaborator.user_id)
- .collect::<Vec<_>>(),
+ user_ids,
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
);
}
@@ -0,0 +1,1306 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use collab_ui::project_shared_notification::ProjectSharedNotification;
+use editor::{Editor, ExcerptRange, MultiBuffer};
+use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
+use live_kit_client::MacOSDisplay;
+use serde_json::json;
+use std::{borrow::Cow, sync::Arc};
+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(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+ cx_d: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+
+ let mut server = TestServer::start(&deterministic).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);
+ let window_b = client_b.build_workspace(&project_b, cx_b);
+ let workspace_b = window_b.root(cx_b);
+
+ // Client A opens some editors.
+ let pane_a = workspace_a.read_with(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.foreground().run_until_parked();
+ let editor_b2 = workspace_b.read_with(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.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![2..1]
+ );
+ assert_eq!(
+ editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![3..2]
+ );
+
+ cx_c.foreground().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);
+ 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.foreground().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);
+ 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.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ("D", &active_call_d, &cx_d),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a, project_id),
+ &[peer_id_b, peer_id_c],
+ "checking followers for A as {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.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ("D", &active_call_d, &cx_d),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a, project_id),
+ &[peer_id_b],
+ "checking followers for A as {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.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ("D", &active_call_d, &cx_d),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a, project_id),
+ &[peer_id_b, peer_id_c],
+ "checking followers for A as {name}"
+ );
+ });
+ }
+
+ // Client D follows client C.
+ 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.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ("D", &active_call_d, &cx_d),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_c, project_id),
+ &[peer_id_d],
+ "checking followers for C as {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.foreground().run_until_parked();
+ for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a, project_id),
+ &[peer_id_b],
+ "checking followers for A as {name}"
+ );
+ });
+ }
+
+ // All clients see that no-one is following C
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ("D", &active_call_d, &cx_d),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_c, project_id),
+ &[],
+ "checking followers for C as {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)
+ });
+ deterministic.run_until_parked();
+ workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+ });
+
+ // When client A opens a multibuffer, client B does so as well.
+ let multibuffer_a = cx_a.add_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.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+ workspace.add_item(Box::new(editor.clone()), cx);
+ editor
+ });
+ deterministic.run_until_parked();
+ let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
+ multibuffer_editor_b.read_with(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();
+ deterministic.run_until_parked();
+ workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+ });
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.go_back(workspace.active_pane().downgrade(), cx)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+ });
+
+ workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.go_forward(workspace.active_pane().downgrade(), cx)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.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]));
+ });
+ deterministic.run_until_parked();
+ editor_b1.read_with(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));
+ deterministic.run_until_parked();
+ editor_b1.read_with(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(vec2f(0., 100.), cx);
+ });
+ deterministic.run_until_parked();
+ editor_b1.read_with(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)
+ });
+ deterministic.run_until_parked();
+ assert_eq!(
+ workspace_b.read_with(cx_b, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ editor_b1.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.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ Some(peer_id_b)
+ );
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ editor_a1.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();
+ deterministic.run_until_parked();
+ let shared_screen = workspace_a.read_with(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();
+ deterministic.run_until_parked();
+ workspace_a.read_with(cx_a, |workspace, cx| {
+ assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.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)
+ });
+ deterministic.run_until_parked();
+ workspace_a.read_with(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().id(),
+ multibuffer_editor_a.id()
+ )
+ });
+
+ // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
+ let panel = window_b.add_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);
+ });
+ deterministic.run_until_parked();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ shared_screen.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);
+ });
+ deterministic.run_until_parked();
+ workspace_a.read_with(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().id(),
+ multibuffer_editor_a.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.add_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)
+ })
+ });
+ deterministic.run_until_parked();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ shared_screen.id()
+ );
+
+ // Following interrupts when client B disconnects.
+ client_b.disconnect(&cx_b.to_async());
+ deterministic.advance_clock(RECONNECT_TIMEOUT);
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+ None
+ );
+}
+
+#[gpui::test]
+async fn test_following_tab_order(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(&deterministic).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);
+ let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+
+ let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+ let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+
+ let client_b_id = project_a.read_with(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: &ViewHandle<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();
+ deterministic.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"]);
+ deterministic.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(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).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 some editors.
+ let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+ let pane_a1 = workspace_a.read_with(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();
+
+ // Client B opens an editor.
+ let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+ let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+ let _editor_b1 = 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| {
+ assert_ne!(*workspace.active_pane(), pane_a1);
+ let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+ workspace.follow(leader_id, 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| {
+ assert_ne!(*workspace.active_pane(), pane_b1);
+ let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+ workspace.follow(leader_id, cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.activate_next_pane(cx);
+ });
+ // Wait for focus effects to be fully flushed
+ workspace_a.update(cx_a, |workspace, _| {
+ assert_eq!(*workspace.active_pane(), pane_a1);
+ });
+
+ 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.activate_next_pane(cx);
+ });
+
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ assert_eq!(*workspace.active_pane(), pane_b1);
+ workspace.open_path((worktree_id, "4.txt"), None, true, cx)
+ })
+ .await
+ .unwrap();
+ cx_a.foreground().run_until_parked();
+
+ // Ensure leader updates don't change the active pane of followers
+ workspace_a.read_with(cx_a, |workspace, _| {
+ assert_eq!(*workspace.active_pane(), pane_a1);
+ });
+ workspace_b.read_with(cx_b, |workspace, _| {
+ assert_eq!(*workspace.active_pane(), pane_b1);
+ });
+
+ // Ensure peers following each other doesn't cause an infinite loop.
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .project_path(cx)),
+ Some((worktree_id, "3.txt").into())
+ );
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "3.txt").into())
+ );
+ workspace.activate_next_pane(cx);
+ });
+
+ workspace_a.update(cx_a, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "4.txt").into())
+ );
+ });
+
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "4.txt").into())
+ );
+ workspace.activate_next_pane(cx);
+ });
+
+ workspace_b.update(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.active_item(cx).unwrap().project_path(cx),
+ Some((worktree_id, "3.txt").into())
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_auto_unfollowing(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+
+ // 2 clients connect to a server.
+ let mut server = TestServer::start(&deterministic).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();
+
+ // Client A opens some editors.
+ let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+ 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);
+ let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+ let leader_id = project_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ Some(leader_id)
+ );
+ let editor_b2 = workspace_b.read_with(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.read_with(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.read_with(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.read_with(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.read_with(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(vec2f(0., 3.), cx)
+ });
+ assert_eq!(
+ workspace_b.read_with(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.read_with(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.read_with(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.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+ None
+ );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_simultaneously_following_each_other(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+
+ let mut server = TestServer::start(&deterministic).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);
+ 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);
+
+ deterministic.run_until_parked();
+ let client_a_id = project_b.read_with(cx_b, |project, _| {
+ project.collaborators().values().next().unwrap().peer_id
+ });
+ let client_b_id = project_a.read_with(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.read_with(cx_a, |workspace, _| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_b_id)
+ );
+ });
+ workspace_b.read_with(cx_b, |workspace, _| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_a_id)
+ );
+ });
+}
+
+fn visible_push_notifications(
+ cx: &mut TestAppContext,
+) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+ let mut ret = Vec::new();
+ for window in cx.windows() {
+ window.read_with(cx, |window| {
+ if let Some(handle) = window
+ .root_view()
+ .clone()
+ .downcast::<ProjectSharedNotification>()
+ {
+ ret.push(handle)
+ }
+ });
+ }
+ ret
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following_across_workspaces(
+ deterministic: Arc<Deterministic>,
+ 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
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).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);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b).root(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();
+
+ deterministic.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()
+ });
+
+ deterministic.run_until_parked();
+ let workspace_b_project_a = cx_b
+ .windows()
+ .iter()
+ .max_by_key(|window| window.id())
+ .unwrap()
+ .downcast::<Workspace>()
+ .unwrap()
+ .root(cx_b);
+
+ // 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();
+
+ deterministic.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()
+ });
+
+ deterministic.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(), Cow::Borrowed("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();
+
+ deterministic.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();
+
+ deterministic.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.id())
+ .unwrap()
+ .downcast::<Workspace>()
+ .unwrap()
+ .root(cx_a);
+
+ 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"));
+ });
+}
@@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
use editor::{
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
- ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
+ ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
};
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
-use gpui::{
- executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
- TestAppContext, ViewHandle,
-};
+use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
@@ -38,12 +35,7 @@ use std::{
},
};
use unindent::Unindent as _;
-use workspace::{
- dock::{test::TestPanel, DockPosition},
- item::{test::TestItem, ItemHandle as _},
- shared_screen::SharedScreen,
- SplitDirection, Workspace,
-};
+use workspace::Workspace;
#[ctor::ctor]
fn init_logger() {
@@ -6388,455 +6380,49 @@ async fn test_contact_requests(
}
#[gpui::test(iterations = 10)]
-async fn test_basic_following(
+async fn test_join_call_after_screen_was_shared(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
- cx_c: &mut TestAppContext,
- cx_d: &mut TestAppContext,
) {
deterministic.forbid_parking();
-
let mut server = TestServer::start(&deterministic).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),
- ])
+ .make_contacts(&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\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;
+ // Call users B and C from client A.
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);
- let window_b = client_b.build_workspace(&project_b, cx_b);
- let workspace_b = window_b.root(cx_b);
-
- // Client A opens some editors.
- let pane_a = workspace_a.read_with(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.toggle_follow(peer_id_a, cx).unwrap()
- })
- .await
- .unwrap();
-
- cx_c.foreground().run_until_parked();
- let editor_b2 = workspace_b.read_with(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.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![2..1]
- );
- assert_eq!(
- editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![3..2]
- );
-
- cx_c.foreground().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);
- 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.toggle_follow(peer_id_a, cx).unwrap()
- })
- .await
- .unwrap();
-
- cx_d.foreground().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);
- 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.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
- }
-
- // Client C unfollows client A.
- workspace_c.update(cx_c, |workspace, cx| {
- workspace.toggle_follow(peer_id_a, cx);
- });
-
- // All clients see that clients B is following client A.
- cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
- }
-
- // Client C re-follows client A.
- workspace_c.update(cx_c, |workspace, cx| {
- workspace.toggle_follow(peer_id_a, cx);
- });
-
- // All clients see that clients B and C are following client A.
- cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
- }
-
- // Client D follows client C.
- workspace_d
- .update(cx_d, |workspace, cx| {
- workspace.toggle_follow(peer_id_c, cx).unwrap()
- })
- .await
- .unwrap();
-
- // All clients see that D is following C
- cx_d.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[peer_id_d],
- "checking followers for C as {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.foreground().run_until_parked();
- for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
- }
-
- // All clients see that no-one is following C
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[],
- "checking followers for C as {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)
- });
- deterministic.run_until_parked();
- workspace_b.read_with(cx_b, |workspace, cx| {
- assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
- });
-
- // When client A opens a multibuffer, client B does so as well.
- let multibuffer_a = cx_a.add_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.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
- workspace.add_item(Box::new(editor.clone()), cx);
- editor
- });
- deterministic.run_until_parked();
- let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
- workspace
- .active_item(cx)
- .unwrap()
- .downcast::<Editor>()
- .unwrap()
- });
- assert_eq!(
- multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
- multibuffer_editor_b.read_with(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();
- deterministic.run_until_parked();
- workspace_b.read_with(cx_b, |workspace, cx| {
- assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
- });
-
- workspace_a
- .update(cx_a, |workspace, cx| {
- workspace.go_back(workspace.active_pane().downgrade(), cx)
- })
- .await
- .unwrap();
- deterministic.run_until_parked();
- workspace_b.read_with(cx_b, |workspace, cx| {
- assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
- });
-
- workspace_a
- .update(cx_a, |workspace, cx| {
- workspace.go_forward(workspace.active_pane().downgrade(), cx)
+ .update(cx_a, |call, cx| {
+ call.invite(client_b.user_id().unwrap(), None, cx)
})
.await
.unwrap();
- deterministic.run_until_parked();
- workspace_b.read_with(cx_b, |workspace, cx| {
- assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.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]));
- });
- deterministic.run_until_parked();
- editor_b1.read_with(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));
- deterministic.run_until_parked();
- editor_b1.read_with(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(vec2f(0., 100.), cx);
- });
- deterministic.run_until_parked();
- editor_b1.read_with(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)
- });
+ let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
- workspace_b.read_with(cx_b, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .id()),
- editor_b1.id()
+ room_participants(&room_a, cx_a),
+ RoomParticipants {
+ remote: Default::default(),
+ pending: vec!["user_b".to_string()]
+ }
);
- // Client A starts following client B.
- workspace_a
- .update(cx_a, |workspace, cx| {
- workspace.toggle_follow(peer_id_b, cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
- Some(peer_id_b)
- );
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .id()),
- editor_a1.id()
- );
+ // User B receives the call.
+ let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
+ let call_b = incoming_call_b.next().await.unwrap().unwrap();
+ assert_eq!(call_b.calling_user.github_login, "user_a");
- // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+ // User A shares their screen
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| {
+ active_call_a
+ .update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
@@ -6844,153 +6430,18 @@ async fn test_basic_following(
})
.await
.unwrap();
- deterministic.run_until_parked();
- let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
- workspace
- .active_item(cx)
- .unwrap()
- .downcast::<SharedScreen>()
- .unwrap()
+
+ client_b.user_store().update(cx_b, |user_store, _| {
+ user_store.clear_cache();
});
- // Client B activates Zed again, which causes the previous editor to become focused again.
+ // User B joins the room
active_call_b
- .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
- deterministic.run_until_parked();
- workspace_a.read_with(cx_a, |workspace, cx| {
- assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.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)
- });
- deterministic.run_until_parked();
- workspace_a.read_with(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().id(),
- multibuffer_editor_a.id()
- )
- });
-
- // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
- let panel = window_b.add_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);
- });
- deterministic.run_until_parked();
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .id()),
- shared_screen.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);
- });
- deterministic.run_until_parked();
- workspace_a.read_with(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().id(),
- multibuffer_editor_a.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.add_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)
- })
- });
- deterministic.run_until_parked();
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .id()),
- shared_screen.id()
- );
-
- // Following interrupts when client B disconnects.
- client_b.disconnect(&cx_b.to_async());
- deterministic.advance_clock(RECONNECT_TIMEOUT);
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
- None
- );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_join_call_after_screen_was_shared(
- deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- deterministic.forbid_parking();
- let mut server = TestServer::start(&deterministic).await;
-
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
- server
- .make_contacts(&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);
-
- // Call users B and C from client A.
- active_call_a
- .update(cx_a, |call, cx| {
- call.invite(client_b.user_id().unwrap(), None, cx)
- })
- .await
- .unwrap();
- let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
- deterministic.run_until_parked();
- assert_eq!(
- room_participants(&room_a, cx_a),
- RoomParticipants {
- remote: Default::default(),
- pending: vec!["user_b".to_string()]
- }
- );
-
- // User B receives the call.
- let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
- let call_b = incoming_call_b.next().await.unwrap().unwrap();
- assert_eq!(call_b.calling_user.github_login, "user_a");
-
- // User A shares their screen
- let display = MacOSDisplay::new();
- active_call_a
- .update(cx_a, |call, cx| {
- call.room().unwrap().update(cx, |room, cx| {
- room.set_display_sources(vec![display.clone()]);
- room.share_screen(cx)
- })
- })
- .await
- .unwrap();
-
- client_b.user_store().update(cx_b, |user_store, _| {
- user_store.clear_cache();
- });
-
- // User B joins the room
- active_call_b
- .update(cx_b, |call, cx| call.accept_incoming(cx))
- .await
- .unwrap();
- let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
- assert!(incoming_call_b.next().await.unwrap().is_none());
+ let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+ assert!(incoming_call_b.next().await.unwrap().is_none());
deterministic.run_until_parked();
assert_eq!(
@@ -7021,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared(
});
}
-#[gpui::test]
-async fn test_following_tab_order(
- deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- let mut server = TestServer::start(&deterministic).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);
- let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-
- let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
- let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-
- let client_b_id = project_a.read_with(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: &ViewHandle<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.toggle_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();
- deterministic.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"]);
- deterministic.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(
- deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- deterministic.forbid_parking();
- let mut server = TestServer::start(&deterministic).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 some editors.
- let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
- let pane_a1 = workspace_a.read_with(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();
-
- // Client B opens an editor.
- let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
- let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
- let _editor_b1 = 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| {
- assert_ne!(*workspace.active_pane(), pane_a1);
- let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
- workspace.toggle_follow(leader_id, 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| {
- assert_ne!(*workspace.active_pane(), pane_b1);
- let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
- workspace.toggle_follow(leader_id, cx).unwrap()
- })
- .await
- .unwrap();
-
- workspace_a.update(cx_a, |workspace, cx| {
- workspace.activate_next_pane(cx);
- });
- // Wait for focus effects to be fully flushed
- workspace_a.update(cx_a, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_a1);
- });
-
- 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.activate_next_pane(cx);
- });
-
- workspace_b
- .update(cx_b, |workspace, cx| {
- assert_eq!(*workspace.active_pane(), pane_b1);
- workspace.open_path((worktree_id, "4.txt"), None, true, cx)
- })
- .await
- .unwrap();
- cx_a.foreground().run_until_parked();
-
- // Ensure leader updates don't change the active pane of followers
- workspace_a.read_with(cx_a, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_a1);
- });
- workspace_b.read_with(cx_b, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_b1);
- });
-
- // Ensure peers following each other doesn't cause an infinite loop.
- assert_eq!(
- workspace_a.read_with(cx_a, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .project_path(cx)),
- Some((worktree_id, "3.txt").into())
- );
- workspace_a.update(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "3.txt").into())
- );
- workspace.activate_next_pane(cx);
- });
-
- workspace_a.update(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "4.txt").into())
- );
- });
-
- workspace_b.update(cx_b, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "4.txt").into())
- );
- workspace.activate_next_pane(cx);
- });
-
- workspace_b.update(cx_b, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "3.txt").into())
- );
- });
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_auto_unfollowing(
- deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- deterministic.forbid_parking();
-
- // 2 clients connect to a server.
- let mut server = TestServer::start(&deterministic).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();
-
- // Client A opens some editors.
- let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
- 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);
- let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
- let leader_id = project_b.read_with(cx_b, |project, _| {
- project.collaborators().values().next().unwrap().peer_id
- });
- workspace_b
- .update(cx_b, |workspace, cx| {
- workspace.toggle_follow(leader_id, cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(
- workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
- Some(leader_id)
- );
- let editor_b2 = workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
- None
- );
-
- workspace_b
- .update(cx_b, |workspace, cx| {
- workspace.toggle_follow(leader_id, cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(
- workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
- None
- );
-
- workspace_b
- .update(cx_b, |workspace, cx| {
- workspace.toggle_follow(leader_id, cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(
- workspace_b.read_with(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(vec2f(0., 3.), cx)
- });
- assert_eq!(
- workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
- None
- );
-
- workspace_b
- .update(cx_b, |workspace, cx| {
- workspace.toggle_follow(leader_id, cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(
- workspace_b.read_with(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.read_with(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.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
- None
- );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_peers_simultaneously_following_each_other(
- deterministic: Arc<Deterministic>,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- deterministic.forbid_parking();
-
- let mut server = TestServer::start(&deterministic).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);
- 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);
-
- deterministic.run_until_parked();
- let client_a_id = project_b.read_with(cx_b, |project, _| {
- project.collaborators().values().next().unwrap().peer_id
- });
- let client_b_id = project_a.read_with(cx_a, |project, _| {
- project.collaborators().values().next().unwrap().peer_id
- });
-
- let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
- workspace.toggle_follow(client_b_id, cx).unwrap()
- });
- let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
- workspace.toggle_follow(client_a_id, cx).unwrap()
- });
-
- futures::try_join!(a_follow_b, b_follow_a).unwrap();
- workspace_a.read_with(cx_a, |workspace, _| {
- assert_eq!(
- workspace.leader_for_pane(workspace.active_pane()),
- Some(client_b_id)
- );
- });
- workspace_b.read_with(cx_b, |workspace, _| {
- assert_eq!(
- workspace.leader_for_pane(workspace.active_pane()),
- Some(client_a_id)
- );
- });
-}
-
#[gpui::test(iterations = 10)]
async fn test_on_input_format_from_host_to_guest(
deterministic: Arc<Deterministic>,
@@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest {
// channel buffer.
let collaborators = channel_buffer.collaborators();
let mut user_ids =
- collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
+ collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
user_ids,
@@ -29,7 +29,7 @@ use std::{
},
};
use util::http::FakeHttpClient;
-use workspace::Workspace;
+use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
pub app_state: Arc<AppState>,
@@ -204,13 +204,17 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+ let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ let mut language_registry = LanguageRegistry::test();
+ language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
+ workspace_store,
channel_store: channel_store.clone(),
- languages: Arc::new(LanguageRegistry::test()),
+ languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -536,15 +540,7 @@ impl TestClient {
root_path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, WorktreeId) {
- let project = cx.update(|cx| {
- Project::local(
- self.client().clone(),
- self.app_state.user_store.clone(),
- self.app_state.languages.clone(),
- self.app_state.fs.clone(),
- cx,
- )
- });
+ let project = self.build_empty_local_project(cx);
let (worktree, _) = project
.update(cx, |p, cx| {
p.find_or_create_local_worktree(root_path, true, cx)
@@ -557,6 +553,18 @@ impl TestClient {
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
+ pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
+ cx.update(|cx| {
+ Project::local(
+ self.client().clone(),
+ self.app_state.user_store.clone(),
+ self.app_state.languages.clone(),
+ self.app_state.fs.clone(),
+ cx,
+ )
+ })
+ }
+
pub async fn build_remote_project(
&self,
host_project_id: u64,
@@ -1,10 +1,12 @@
use anyhow::{anyhow, Result};
use call::report_call_event_for_channel;
-use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
-use client::proto;
-use clock::ReplicaId;
+use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
+use client::{
+ proto::{self, PeerId},
+ Collaborator, ParticipantIndex,
+};
use collections::HashMap;
-use editor::Editor;
+use editor::{CollaborationHub, Editor};
use gpui::{
actions,
elements::{ChildView, Label},
@@ -13,7 +15,11 @@ use gpui::{
ViewContext, ViewHandle,
};
use project::Project;
-use std::any::{Any, TypeId};
+use std::{
+ any::{Any, TypeId},
+ sync::Arc,
+};
+use util::ResultExt;
use workspace::{
item::{FollowableItem, Item, ItemHandle},
register_followable_item,
@@ -23,7 +29,7 @@ use workspace::{
actions!(channel_view, [Deploy]);
-pub(crate) fn init(cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
}
@@ -36,9 +42,13 @@ pub struct ChannelView {
}
impl ChannelView {
- pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
+ pub fn open(
+ channel_id: ChannelId,
+ workspace: ViewHandle<Workspace>,
+ cx: &mut AppContext,
+ ) -> Task<Result<ViewHandle<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
- let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
+ let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
@@ -48,14 +58,13 @@ impl ChannelView {
&workspace.read(cx).app_state().client,
cx,
);
- pane.add_item(Box::new(channel_view), true, true, None, cx);
+ pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
});
- anyhow::Ok(())
+ anyhow::Ok(channel_view)
})
- .detach();
}
- pub fn open(
+ pub fn open_in_pane(
channel_id: ChannelId,
pane: ViewHandle<Pane>,
workspace: ViewHandle<Workspace>,
@@ -74,12 +83,13 @@ impl ChannelView {
cx.spawn(|mut cx| async move {
let channel_buffer = channel_buffer.await?;
- let markdown = markdown.await?;
- channel_buffer.update(&mut cx, |buffer, cx| {
- buffer.buffer().update(cx, |buffer, cx| {
- buffer.set_language(Some(markdown), cx);
- })
- });
+ if let Some(markdown) = markdown.await.log_err() {
+ channel_buffer.update(&mut cx, |buffer, cx| {
+ buffer.buffer().update(cx, |buffer, cx| {
+ buffer.set_language(Some(markdown), cx);
+ })
+ });
+ }
pane.update(&mut cx, |pane, cx| {
pane.items_of_type::<Self>()
@@ -96,40 +106,29 @@ impl ChannelView {
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
- let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+ let editor = cx.add_view(|cx| {
+ let mut editor = Editor::for_buffer(buffer, None, cx);
+ editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+ channel_buffer.clone(),
+ )));
+ editor
+ });
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
- cx.subscribe(&project, Self::handle_project_event).detach();
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
.detach();
- let this = Self {
+ Self {
editor,
project,
channel_buffer,
remote_id: None,
_editor_event_subscription,
- };
- this.refresh_replica_id_map(cx);
- this
+ }
}
- fn handle_project_event(
- &mut self,
- _: ModelHandle<Project>,
- event: &project::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- project::Event::RemoteIdChanged(_) => {}
- project::Event::DisconnectedFromHost => {}
- project::Event::Closed => {}
- project::Event::CollaboratorUpdated { .. } => {}
- project::Event::CollaboratorLeft(_) => {}
- project::Event::CollaboratorJoined(_) => {}
- _ => return,
- }
- self.refresh_replica_id_map(cx);
+ pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
+ self.channel_buffer.read(cx).channel()
}
fn handle_channel_buffer_event(
@@ -138,51 +137,13 @@ impl ChannelView {
event: &ChannelBufferEvent,
cx: &mut ViewContext<Self>,
) {
- match event {
- ChannelBufferEvent::CollaboratorsChanged => {
- self.refresh_replica_id_map(cx);
- }
- ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
+ if let ChannelBufferEvent::Disconnected = event {
+ self.editor.update(cx, |editor, cx| {
editor.set_read_only(true);
cx.notify();
- }),
+ })
}
}
-
- /// Build a mapping of channel buffer replica ids to the corresponding
- /// replica ids in the current project.
- ///
- /// Using this mapping, a given user can be displayed with the same color
- /// in the channel buffer as in other files in the project. Users who are
- /// in the channel buffer but not the project will not have a color.
- fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
- let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
- let project = self.project.read(cx);
- let channel_buffer = self.channel_buffer.read(cx);
- project_replica_ids_by_channel_buffer_replica_id
- .insert(channel_buffer.replica_id(cx), project.replica_id());
- project_replica_ids_by_channel_buffer_replica_id.extend(
- channel_buffer
- .collaborators()
- .iter()
- .filter_map(|channel_buffer_collaborator| {
- project
- .collaborators()
- .values()
- .find_map(|project_collaborator| {
- (project_collaborator.user_id == channel_buffer_collaborator.user_id)
- .then_some((
- channel_buffer_collaborator.replica_id as ReplicaId,
- project_collaborator.replica_id,
- ))
- })
- }),
- );
-
- self.editor.update(cx, |editor, cx| {
- editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
- });
- }
}
impl Entity for ChannelView {
@@ -311,7 +272,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
- let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+ let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
@@ -371,17 +332,32 @@ impl FollowableItem for ChannelView {
})
}
- fn set_leader_replica_id(
- &mut self,
- leader_replica_id: Option<u16>,
- cx: &mut ViewContext<Self>,
- ) {
+ fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
- editor.set_leader_replica_id(leader_replica_id, cx)
+ editor.set_leader_peer_id(leader_peer_id, cx)
})
}
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
Editor::should_unfollow_on_event(event, cx)
}
+
+ fn is_project_item(&self, _cx: &AppContext) -> bool {
+ false
+ }
+}
+
+struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+impl CollaborationHub for ChannelBufferCollaborationHub {
+ fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+ self.0.read(cx).collaborators()
+ }
+
+ fn user_participant_indices<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> &'a HashMap<u64, ParticipantIndex> {
+ self.0.read(cx).user_store().read(cx).participant_indices()
+ }
}
@@ -409,7 +409,7 @@ impl ChatPanel {
})
.on_click(MouseButton::Left, move |_, _, cx| {
if let Some(workspace) = workspace.upgrade(cx) {
- ChannelView::deploy(channel_id, workspace, cx);
+ ChannelView::open(channel_id, workspace, cx).detach();
}
})
.with_tooltip::<OpenChannelNotes>(
@@ -546,7 +546,7 @@ impl ChatPanel {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel().id;
if let Some(workspace) = self.workspace.upgrade(cx) {
- ChannelView::deploy(channel_id, workspace, cx);
+ ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
@@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
item::ItemHandle,
- Workspace,
+ FollowNextCollaborator, Workspace,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -404,6 +404,7 @@ enum ListEntry {
Header(Section),
CallParticipant {
user: Arc<User>,
+ peer_id: Option<PeerId>,
is_pending: bool,
},
ParticipantProject {
@@ -508,14 +509,19 @@ impl CollabPanel {
let is_collapsed = this.collapsed_sections.contains(section);
this.render_header(*section, &theme, is_selected, is_collapsed, cx)
}
- ListEntry::CallParticipant { user, is_pending } => {
- Self::render_call_participant(
- user,
- *is_pending,
- is_selected,
- &theme.collab_panel,
- )
- }
+ ListEntry::CallParticipant {
+ user,
+ peer_id,
+ is_pending,
+ } => Self::render_call_participant(
+ user,
+ *peer_id,
+ this.user_store.clone(),
+ *is_pending,
+ is_selected,
+ &theme,
+ cx,
+ ),
ListEntry::ParticipantProject {
project_id,
worktree_root_names,
@@ -528,7 +534,7 @@ impl CollabPanel {
Some(*project_id) == current_project_id,
*is_last,
is_selected,
- &theme.collab_panel,
+ &theme,
cx,
),
ListEntry::ParticipantScreen { peer_id, is_last } => {
@@ -793,6 +799,7 @@ impl CollabPanel {
let user_id = user.id;
self.entries.push(ListEntry::CallParticipant {
user,
+ peer_id: None,
is_pending: false,
});
let mut projects = room.local_participant().projects.iter().peekable();
@@ -830,6 +837,7 @@ impl CollabPanel {
let participant = &room.remote_participants()[&user_id];
self.entries.push(ListEntry::CallParticipant {
user: participant.user.clone(),
+ peer_id: Some(participant.peer_id),
is_pending: false,
});
let mut projects = participant.projects.iter().peekable();
@@ -871,6 +879,7 @@ impl CollabPanel {
self.entries
.extend(matches.iter().map(|mat| ListEntry::CallParticipant {
user: room.pending_participants()[mat.candidate_id].clone(),
+ peer_id: None,
is_pending: true,
}));
}
@@ -1174,46 +1183,97 @@ impl CollabPanel {
fn render_call_participant(
user: &User,
+ peer_id: Option<PeerId>,
+ user_store: ModelHandle<UserStore>,
is_pending: bool,
is_selected: bool,
- theme: &theme::CollabPanel,
+ theme: &theme::Theme,
+ cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
- Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_children(if is_pending {
- Some(
- Label::new("Calling", theme.calling_indicator.text.clone())
+ enum CallParticipant {}
+ enum CallParticipantTooltip {}
+
+ let collab_theme = &theme.collab_panel;
+
+ let is_current_user =
+ user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+ let content =
+ MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
+ let style = if is_current_user {
+ *collab_theme
+ .contact_row
+ .in_state(is_selected)
+ .style_for(&mut Default::default())
+ } else {
+ *collab_theme
+ .contact_row
+ .in_state(is_selected)
+ .style_for(mouse_state)
+ };
+
+ Flex::row()
+ .with_children(user.avatar.clone().map(|avatar| {
+ Image::from_data(avatar)
+ .with_style(collab_theme.contact_avatar)
+ .aligned()
+ .left()
+ }))
+ .with_child(
+ Label::new(
+ user.github_login.clone(),
+ collab_theme.contact_username.text.clone(),
+ )
.contained()
- .with_style(theme.calling_indicator.container)
- .aligned(),
- )
- } else {
- None
+ .with_style(collab_theme.contact_username.container)
+ .aligned()
+ .left()
+ .flex(1., true),
+ )
+ .with_children(if is_pending {
+ Some(
+ Label::new("Calling", collab_theme.calling_indicator.text.clone())
+ .contained()
+ .with_style(collab_theme.calling_indicator.container)
+ .aligned(),
+ )
+ } else if is_current_user {
+ Some(
+ Label::new("You", collab_theme.calling_indicator.text.clone())
+ .contained()
+ .with_style(collab_theme.calling_indicator.container)
+ .aligned(),
+ )
+ } else {
+ None
+ })
+ .constrained()
+ .with_height(collab_theme.row_height)
+ .contained()
+ .with_style(style)
+ });
+
+ if is_current_user || is_pending || peer_id.is_none() {
+ return content.into_any();
+ }
+
+ let tooltip = format!("Follow {}", user.github_login);
+
+ content
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ workspace
+ .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+ .map(|task| task.detach_and_log_err(cx));
+ }
})
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(
- *theme
- .contact_row
- .in_state(is_selected)
- .style_for(&mut Default::default()),
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_tooltip::<CallParticipantTooltip>(
+ user.id as usize,
+ tooltip,
+ Some(Box::new(FollowNextCollaborator)),
+ theme.tooltip.clone(),
+ cx,
)
.into_any()
}
@@ -1225,74 +1285,91 @@ impl CollabPanel {
is_current: bool,
is_last: bool,
is_selected: bool,
- theme: &theme::CollabPanel,
+ theme: &theme::Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum JoinProject {}
+ enum JoinProjectTooltip {}
- let host_avatar_width = theme
+ let collab_theme = &theme.collab_panel;
+ let host_avatar_width = collab_theme
.contact_avatar
.width
- .or(theme.contact_avatar.height)
+ .or(collab_theme.contact_avatar.height)
.unwrap_or(0.);
- let tree_branch = theme.tree_branch;
+ let tree_branch = collab_theme.tree_branch;
let project_name = if worktree_root_names.is_empty() {
"untitled".to_string()
} else {
worktree_root_names.join(", ")
};
- MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
- let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- let row = theme
- .project_row
- .in_state(is_selected)
- .style_for(mouse_state);
+ let content =
+ MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+ let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+ let row = if is_current {
+ collab_theme
+ .project_row
+ .in_state(true)
+ .style_for(&mut Default::default())
+ } else {
+ collab_theme
+ .project_row
+ .in_state(is_selected)
+ .style_for(mouse_state)
+ };
- Flex::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- is_last,
- vec2f(host_avatar_width, theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/file_icons/folder.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new(project_name, row.name.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(row.name.container)
- .flex(1., false),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(row.container)
- })
- .with_cursor_style(if !is_current {
- CursorStyle::PointingHand
- } else {
- CursorStyle::Arrow
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if !is_current {
+ Flex::row()
+ .with_child(render_tree_branch(
+ tree_branch,
+ &row.name.text,
+ is_last,
+ vec2f(host_avatar_width, collab_theme.row_height),
+ cx.font_cache(),
+ ))
+ .with_child(
+ Svg::new("icons/file_icons/folder.svg")
+ .with_color(collab_theme.channel_hash.color)
+ .constrained()
+ .with_width(collab_theme.channel_hash.width)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Label::new(project_name.clone(), row.name.text.clone())
+ .aligned()
+ .left()
+ .contained()
+ .with_style(row.name.container)
+ .flex(1., false),
+ )
+ .constrained()
+ .with_height(collab_theme.row_height)
+ .contained()
+ .with_style(row.container)
+ });
+
+ if is_current {
+ return content.into_any();
+ }
+
+ content
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
}
- }
- })
- .into_any()
+ })
+ .with_tooltip::<JoinProjectTooltip>(
+ project_id as usize,
+ format!("Open {}", project_name),
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any()
}
fn render_participant_screen(
@@ -2755,7 +2832,7 @@ impl CollabPanel {
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
- ChannelView::deploy(action.channel_id, workspace, cx);
+ ChannelView::open(action.channel_id, workspace, cx).detach();
}
}
@@ -215,7 +215,13 @@ impl CollabTitlebarItem {
let git_style = theme.titlebar.git_menu_button.clone();
let item_spacing = theme.titlebar.item_spacing;
- let mut ret = Flex::row().with_child(
+ let mut ret = Flex::row();
+
+ if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+ ret = ret.with_child(project_host)
+ }
+
+ ret = ret.with_child(
Stack::new()
.with_child(
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@@ -283,6 +289,71 @@ impl CollabTitlebarItem {
ret.into_any()
}
+ fn collect_project_host(
+ &self,
+ theme: Arc<Theme>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ if ActiveCall::global(cx).read(cx).room().is_none() {
+ return None;
+ }
+ let project = self.project.read(cx);
+ let user_store = self.user_store.read(cx);
+
+ if project.is_local() {
+ return None;
+ }
+
+ let Some(host) = project.host() else {
+ return None;
+ };
+ let (Some(host_user), Some(participant_index)) = (
+ user_store.get_cached_user(host.user_id),
+ user_store.participant_indices().get(&host.user_id),
+ ) else {
+ return None;
+ };
+
+ enum ProjectHost {}
+ enum ProjectHostTooltip {}
+
+ let host_style = theme.titlebar.project_host.clone();
+ let selection_style = theme
+ .editor
+ .selection_style_for_room_participant(participant_index.0);
+ let peer_id = host.peer_id.clone();
+
+ Some(
+ MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+ let mut host_style = host_style.style_for(mouse_state).clone();
+ host_style.text.color = selection_style.cursor;
+ Label::new(host_user.github_login.clone(), host_style.text)
+ .contained()
+ .with_style(host_style.container)
+ .aligned()
+ .left()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ if let Some(task) =
+ workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+ {
+ task.detach_and_log_err(cx);
+ }
+ }
+ })
+ .with_tooltip::<ProjectHostTooltip>(
+ 0,
+ host_user.github_login.clone() + " is sharing this project. Click to follow.",
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any_named("project-host"),
+ )
+ }
+
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active {
Some(self.project.clone())
@@ -877,7 +948,7 @@ impl CollabTitlebarItem {
fn render_face_pile(
&self,
user: &User,
- replica_id: Option<ReplicaId>,
+ _replica_id: Option<ReplicaId>,
peer_id: PeerId,
location: Option<ParticipantLocation>,
muted: bool,
@@ -886,23 +957,20 @@ impl CollabTitlebarItem {
theme: &Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
+ let user_id = user.id;
let project_id = workspace.read(cx).project().read(cx).remote_id();
- let room = ActiveCall::global(cx).read(cx).room();
- let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
- let followed_by_self = room
- .and_then(|room| {
- Some(
- is_being_followed
- && room
- .read(cx)
- .followers_for(peer_id, project_id?)
- .iter()
- .any(|&follower| {
- Some(follower) == workspace.read(cx).client().peer_id()
- }),
- )
- })
- .unwrap_or(false);
+ let room = ActiveCall::global(cx).read(cx).room().cloned();
+ let self_peer_id = workspace.read(cx).client().peer_id();
+ let self_following = workspace.read(cx).is_being_followed(peer_id);
+ let self_following_initialized = self_following
+ && room.as_ref().map_or(false, |room| match project_id {
+ None => true,
+ Some(project_id) => room
+ .read(cx)
+ .followers_for(peer_id, project_id)
+ .iter()
+ .any(|&follower| Some(follower) == self_peer_id),
+ });
let leader_style = theme.titlebar.leader_avatar;
let follower_style = theme.titlebar.follower_avatar;
@@ -921,147 +989,131 @@ impl CollabTitlebarItem {
.background_color
.unwrap_or_default();
- if let Some(replica_id) = replica_id {
- if followed_by_self {
- let selection = theme.editor.replica_selection_style(replica_id).selection;
+ let participant_index = self
+ .user_store
+ .read(cx)
+ .participant_indices()
+ .get(&user_id)
+ .copied();
+ if let Some(participant_index) = participant_index {
+ if self_following_initialized {
+ let selection = theme
+ .editor
+ .selection_style_for_room_participant(participant_index.0)
+ .selection;
background_color = Color::blend(selection, background_color);
background_color.a = 255;
}
}
- let mut content = Stack::new()
- .with_children(user.avatar.as_ref().map(|avatar| {
- let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
- .with_child(Self::render_face(
- avatar.clone(),
- Self::location_style(workspace, location, leader_style, cx),
- background_color,
- microphone_state,
- ))
- .with_children(
- (|| {
- let project_id = project_id?;
- let room = room?.read(cx);
- let followers = room.followers_for(peer_id, project_id);
-
- Some(followers.into_iter().flat_map(|&follower| {
- let remote_participant =
- room.remote_participant_for_peer_id(follower);
-
- let avatar = remote_participant
- .and_then(|p| p.user.avatar.clone())
- .or_else(|| {
- if follower == workspace.read(cx).client().peer_id()? {
- workspace
- .read(cx)
- .user_store()
- .read(cx)
- .current_user()?
- .avatar
- .clone()
- } else {
- None
- }
- })?;
-
- Some(Self::render_face(
- avatar.clone(),
- follower_style,
- background_color,
- None,
- ))
- }))
- })()
- .into_iter()
- .flatten(),
- );
-
- let mut container = face_pile
- .contained()
- .with_style(theme.titlebar.leader_selection);
-
- if let Some(replica_id) = replica_id {
- if followed_by_self {
- let color = theme.editor.replica_selection_style(replica_id).selection;
- container = container.with_background_color(color);
- }
- }
-
- container
- }))
- .with_children((|| {
- let replica_id = replica_id?;
- let color = theme.editor.replica_selection_style(replica_id).cursor;
- Some(
- AvatarRibbon::new(color)
- .constrained()
- .with_width(theme.titlebar.avatar_ribbon.width)
- .with_height(theme.titlebar.avatar_ribbon.height)
- .aligned()
- .bottom(),
- )
- })())
- .into_any();
-
- if let Some(location) = location {
- if let Some(replica_id) = replica_id {
- enum ToggleFollow {}
+ enum TitlebarParticipant {}
- content = MouseEventHandler::new::<ToggleFollow, _>(
- replica_id.into(),
- cx,
- move |_, _| content,
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, item, cx| {
- if let Some(workspace) = item.workspace.upgrade(cx) {
- if let Some(task) = workspace
- .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
+ let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+ peer_id.as_u64() as usize,
+ cx,
+ move |_, cx| {
+ Stack::new()
+ .with_children(user.avatar.as_ref().map(|avatar| {
+ let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
+ .with_child(Self::render_face(
+ avatar.clone(),
+ Self::location_style(workspace, location, leader_style, cx),
+ background_color,
+ microphone_state,
+ ))
+ .with_children(
+ (|| {
+ let project_id = project_id?;
+ let room = room?.read(cx);
+ let followers = room.followers_for(peer_id, project_id);
+ Some(followers.into_iter().filter_map(|&follower| {
+ if Some(follower) == self_peer_id {
+ return None;
+ }
+ let participant =
+ room.remote_participant_for_peer_id(follower)?;
+ Some(Self::render_face(
+ participant.user.avatar.clone()?,
+ follower_style,
+ background_color,
+ None,
+ ))
+ }))
+ })()
+ .into_iter()
+ .flatten(),
+ )
+ .with_children(
+ self_following_initialized
+ .then(|| self.user_store.read(cx).current_user())
+ .and_then(|user| {
+ Some(Self::render_face(
+ user?.avatar.clone()?,
+ follower_style,
+ background_color,
+ None,
+ ))
+ }),
+ );
+
+ let mut container = face_pile
+ .contained()
+ .with_style(theme.titlebar.leader_selection);
+
+ if let Some(participant_index) = participant_index {
+ if self_following_initialized {
+ let color = theme
+ .editor
+ .selection_style_for_room_participant(participant_index.0)
+ .selection;
+ container = container.with_background_color(color);
+ }
}
- }
- })
- .with_tooltip::<ToggleFollow>(
- peer_id.as_u64() as usize,
- if is_being_followed {
- format!("Unfollow {}", user.github_login)
- } else {
- format!("Follow {}", user.github_login)
- },
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any();
- } else if let ParticipantLocation::SharedProject { project_id } = location {
- enum JoinProject {}
- let user_id = user.id;
- content = MouseEventHandler::new::<JoinProject, _>(
- peer_id.as_u64() as usize,
- cx,
- move |_, _| content,
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(project_id, user_id, app_state, cx)
- .detach_and_log_err(cx);
- }
- })
- .with_tooltip::<JoinProject>(
- peer_id.as_u64() as usize,
- format!("Follow {} into external project", user.github_login),
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any();
- }
+ container
+ }))
+ .with_children((|| {
+ let participant_index = participant_index?;
+ let color = theme
+ .editor
+ .selection_style_for_room_participant(participant_index.0)
+ .cursor;
+ Some(
+ AvatarRibbon::new(color)
+ .constrained()
+ .with_width(theme.titlebar.avatar_ribbon.width)
+ .with_height(theme.titlebar.avatar_ribbon.height)
+ .aligned()
+ .bottom(),
+ )
+ })())
+ },
+ );
+
+ if Some(peer_id) == self_peer_id {
+ return content.into_any();
}
+
content
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ let Some(workspace) = this.workspace.upgrade(cx) else {
+ return;
+ };
+ if let Some(task) =
+ workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+ {
+ task.detach_and_log_err(cx);
+ }
+ })
+ .with_tooltip::<TitlebarParticipant>(
+ peer_id.as_u64() as usize,
+ format!("Follow {}", user.github_login),
+ Some(Box::new(FollowNextCollaborator)),
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any()
}
fn location_style(
@@ -7,7 +7,7 @@ mod face_pile;
mod incoming_call_notification;
mod notifications;
mod panel_settings;
-mod project_shared_notification;
+pub mod project_shared_notification;
mod sharing_status_indicator;
use call::{report_call_event_for_room, ActiveCall, Room};
@@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.push(window);
}
}
- room::Event::RemoteProjectUnshared { project_id } => {
+ room::Event::RemoteProjectUnshared { project_id }
+ | room::Event::RemoteProjectJoined { project_id }
+ | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
@@ -82,7 +84,6 @@ impl ProjectSharedNotification {
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
- cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
@@ -90,7 +91,15 @@ impl ProjectSharedNotification {
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
- cx.remove_window();
+ if let Some(active_room) =
+ ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
+ {
+ active_room.update(cx, |_, cx| {
+ cx.emit(room::Event::RemoteProjectInvitationDiscarded {
+ project_id: self.project_id,
+ });
+ });
+ }
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context, Result};
use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, TelemetrySettings};
+use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
@@ -79,6 +79,7 @@ pub use multi_buffer::{
use ordered_float::OrderedFloat;
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::{seq::SliceRandom, thread_rng};
+use rpc::proto::PeerId;
use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
@@ -581,11 +582,11 @@ pub struct Editor {
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<ModelHandle<Project>>,
+ collaboration_hub: Option<Box<dyn CollaborationHub>>,
focused: bool,
blink_manager: ModelHandle<BlinkManager>,
pub show_local_selections: bool,
mode: EditorMode,
- replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
show_gutter: bool,
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
@@ -609,7 +610,7 @@ pub struct Editor {
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
read_only: bool,
- leader_replica_id: Option<u16>,
+ leader_peer_id: Option<PeerId>,
remote_id: Option<ViewId>,
hover_state: HoverState,
gutter_hovered: bool,
@@ -631,6 +632,15 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
+pub struct RemoteSelection {
+ pub replica_id: ReplicaId,
+ pub selection: Selection<Anchor>,
+ pub cursor_shape: CursorShape,
+ pub peer_id: PeerId,
+ pub line_mode: bool,
+ pub participant_index: Option<ParticipantIndex>,
+}
+
#[derive(Clone, Debug)]
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
@@ -1047,7 +1057,8 @@ impl CompletionsMenu {
item_ix: Some(item_ix),
},
cx,
- );
+ )
+ .map(|task| task.detach());
})
.into_any(),
);
@@ -1539,12 +1550,12 @@ impl Editor {
active_diagnostics: None,
soft_wrap_mode_override,
get_field_editor_theme,
+ collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project,
focused: false,
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
- replica_id_mapping: None,
show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
placeholder_text: None,
@@ -1571,7 +1582,7 @@ impl Editor {
keymap_context_layers: Default::default(),
input_enabled: true,
read_only: false,
- leader_replica_id: None,
+ leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
@@ -1658,8 +1669,8 @@ impl Editor {
self.buffer.read(cx).replica_id()
}
- pub fn leader_replica_id(&self) -> Option<ReplicaId> {
- self.leader_replica_id
+ pub fn leader_peer_id(&self) -> Option<PeerId> {
+ self.leader_peer_id
}
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
@@ -1723,6 +1734,14 @@ impl Editor {
self.mode
}
+ pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+ self.collaboration_hub.as_deref()
+ }
+
+ pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+ self.collaboration_hub = Some(hub);
+ }
+
pub fn set_placeholder_text(
&mut self,
placeholder_text: impl Into<Arc<str>>,
@@ -1799,26 +1818,13 @@ impl Editor {
cx.notify();
}
- pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
- self.replica_id_mapping.as_ref()
- }
-
- pub fn set_replica_id_map(
- &mut self,
- mapping: Option<HashMap<ReplicaId, ReplicaId>>,
- cx: &mut ViewContext<Self>,
- ) {
- self.replica_id_mapping = mapping;
- cx.notify();
- }
-
fn selections_did_change(
&mut self,
local: bool,
old_cursor_position: &Anchor,
cx: &mut ViewContext<Self>,
) {
- if self.focused && self.leader_replica_id.is_none() {
+ if self.focused && self.leader_peer_id.is_none() {
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
@@ -8625,6 +8631,27 @@ impl Editor {
}
}
+pub trait CollaborationHub {
+ fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
+ fn user_participant_indices<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> &'a HashMap<u64, ParticipantIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+ fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+ self.read(cx).collaborators()
+ }
+
+ fn user_participant_indices<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> &'a HashMap<u64, ParticipantIndex> {
+ self.read(cx).user_store().read(cx).participant_indices()
+ }
+}
+
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
@@ -8668,6 +8695,34 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
}
impl EditorSnapshot {
+ pub fn remote_selections_in_range<'a>(
+ &'a self,
+ range: &'a Range<Anchor>,
+ collaboration_hub: &dyn CollaborationHub,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = RemoteSelection> {
+ let participant_indices = collaboration_hub.user_participant_indices(cx);
+ let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
+ let collaborators_by_replica_id = collaborators_by_peer_id
+ .iter()
+ .map(|(_, collaborator)| (collaborator.replica_id, collaborator))
+ .collect::<HashMap<_, _>>();
+ self.buffer_snapshot
+ .remote_selections_in_range(range)
+ .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
+ let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+ let participant_index = participant_indices.get(&collaborator.user_id).copied();
+ Some(RemoteSelection {
+ replica_id,
+ selection,
+ cursor_shape,
+ line_mode,
+ participant_index,
+ peer_id: collaborator.peer_id,
+ })
+ })
+ }
+
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position)
}
@@ -8781,7 +8836,7 @@ impl View for Editor {
self.focused = true;
self.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx);
- if self.leader_replica_id.is_none() {
+ if self.leader_peer_id.is_none() {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
self.selections.line_mode,
@@ -17,7 +17,6 @@ use crate::{
},
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
};
-use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use git::diff::DiffHunkStatus;
use gpui::{
@@ -55,6 +54,7 @@ use std::{
sync::Arc,
};
use text::Point;
+use theme::SelectionStyle;
use workspace::item::Item;
enum FoldMarkers {}
@@ -868,14 +868,7 @@ impl EditorElement {
let corner_radius = 0.15 * layout.position_map.line_height;
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
- for (replica_id, selections) in &layout.selections {
- let replica_id = *replica_id;
- let selection_style = if let Some(replica_id) = replica_id {
- style.replica_selection_style(replica_id)
- } else {
- &style.absent_selection
- };
-
+ for (selection_style, selections) in &layout.selections {
for selection in selections {
self.paint_highlighted_range(
selection.range.clone(),
@@ -2193,7 +2186,7 @@ impl Element<Editor> for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
};
- let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
+ let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
let mut active_rows = BTreeMap::new();
let mut fold_ranges = Vec::new();
let is_singleton = editor.is_singleton(cx);
@@ -2219,35 +2212,6 @@ impl Element<Editor> for EditorElement {
}),
);
- let mut remote_selections = HashMap::default();
- for (replica_id, line_mode, cursor_shape, selection) in snapshot
- .buffer_snapshot
- .remote_selections_in_range(&(start_anchor..end_anchor))
- {
- let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
- mapping.get(&replica_id).copied()
- } else {
- Some(replica_id)
- };
-
- // The local selections match the leader's selections.
- if replica_id.is_some() && replica_id == editor.leader_replica_id {
- continue;
- }
- remote_selections
- .entry(replica_id)
- .or_insert(Vec::new())
- .push(SelectionLayout::new(
- selection,
- line_mode,
- cursor_shape,
- &snapshot.display_snapshot,
- false,
- false,
- ));
- }
- selections.extend(remote_selections);
-
let mut newest_selection_head = None;
if editor.show_local_selections {
@@ -2282,19 +2246,58 @@ impl Element<Editor> for EditorElement {
layouts.push(layout);
}
- // Render the local selections in the leader's color when following.
- let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
- leader_replica_id
- } else {
- let replica_id = editor.replica_id(cx);
- if let Some(mapping) = &editor.replica_id_mapping {
- mapping.get(&replica_id).copied().unwrap_or(replica_id)
+ selections.push((style.selection, layouts));
+ }
+
+ if let Some(collaboration_hub) = &editor.collaboration_hub {
+ // When following someone, render the local selections in their color.
+ if let Some(leader_id) = editor.leader_peer_id {
+ if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+ if let Some(participant_index) = collaboration_hub
+ .user_participant_indices(cx)
+ .get(&collaborator.user_id)
+ {
+ if let Some((local_selection_style, _)) = selections.first_mut() {
+ *local_selection_style =
+ style.selection_style_for_room_participant(participant_index.0);
+ }
+ }
+ }
+ }
+
+ let mut remote_selections = HashMap::default();
+ for selection in snapshot.remote_selections_in_range(
+ &(start_anchor..end_anchor),
+ collaboration_hub.as_ref(),
+ cx,
+ ) {
+ let selection_style = if let Some(participant_index) = selection.participant_index {
+ style.selection_style_for_room_participant(participant_index.0)
} else {
- replica_id
+ style.absent_selection
+ };
+
+ // Don't re-render the leader's selections, since the local selections
+ // match theirs.
+ if Some(selection.peer_id) == editor.leader_peer_id {
+ continue;
}
- };
- selections.push((Some(local_replica_id), layouts));
+ remote_selections
+ .entry(selection.replica_id)
+ .or_insert((selection_style, Vec::new()))
+ .1
+ .push(SelectionLayout::new(
+ selection.selection,
+ selection.line_mode,
+ selection.cursor_shape,
+ &snapshot.display_snapshot,
+ false,
+ false,
+ ));
+ }
+
+ selections.extend(remote_selections.into_values());
}
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2686,7 +2689,7 @@ pub struct LayoutState {
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
- selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
+ selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
is_singleton: bool,
@@ -17,7 +17,7 @@ use language::{
SelectionGoal,
};
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
-use rpc::proto::{self, update_view};
+use rpc::proto::{self, update_view, PeerId};
use smallvec::SmallVec;
use std::{
borrow::Cow,
@@ -156,13 +156,9 @@ impl FollowableItem for Editor {
}))
}
- fn set_leader_replica_id(
- &mut self,
- leader_replica_id: Option<u16>,
- cx: &mut ViewContext<Self>,
- ) {
- self.leader_replica_id = leader_replica_id;
- if self.leader_replica_id.is_some() {
+ fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+ self.leader_peer_id = leader_peer_id;
+ if self.leader_peer_id.is_some() {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx);
});
@@ -309,6 +305,10 @@ impl FollowableItem for Editor {
_ => false,
}
}
+
+ fn is_project_item(&self, _cx: &AppContext) -> bool {
+ true
+ }
}
async fn update_editor_from_message(
@@ -33,7 +33,7 @@ lazy_static.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
-sysinfo = "0.27.1"
+sysinfo.workspace = true
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
@@ -9,8 +9,6 @@ path = "src/fs.rs"
[dependencies]
collections = { path = "../collections" }
-gpui = { path = "../gpui" }
-lsp = { path = "../lsp" }
rope = { path = "../rope" }
text = { path = "../text" }
util = { path = "../util" }
@@ -34,8 +32,10 @@ log.workspace = true
libc = "0.2"
time.workspace = true
+gpui = { path = "../gpui", optional = true}
+
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
[features]
-test-support = []
+test-support = ["gpui/test-support"]
@@ -93,33 +93,6 @@ pub struct Metadata {
pub is_dir: bool,
}
-impl From<lsp::CreateFileOptions> for CreateOptions {
- fn from(options: lsp::CreateFileOptions) -> Self {
- Self {
- overwrite: options.overwrite.unwrap_or(false),
- ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
- }
- }
-}
-
-impl From<lsp::RenameFileOptions> for RenameOptions {
- fn from(options: lsp::RenameFileOptions) -> Self {
- Self {
- overwrite: options.overwrite.unwrap_or(false),
- ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
- }
- }
-}
-
-impl From<lsp::DeleteFileOptions> for RemoveOptions {
- fn from(options: lsp::DeleteFileOptions) -> Self {
- Self {
- recursive: options.recursive.unwrap_or(false),
- ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
- }
- }
-}
-
pub struct RealFs;
#[async_trait::async_trait]
@@ -11,7 +11,7 @@ path = "src/gpui.rs"
doctest = false
[features]
-test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"]
+test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
[dependencies]
collections = { path = "../collections" }
@@ -103,6 +103,7 @@ pub struct Platform {
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+ active_screen: Screen,
}
impl Platform {
@@ -113,6 +114,7 @@ impl Platform {
current_clipboard_item: Default::default(),
cursor: Mutex::new(CursorStyle::Arrow),
active_window: Default::default(),
+ active_screen: Screen::new(),
}
}
}
@@ -136,12 +138,16 @@ impl super::Platform for Platform {
fn quit(&self) {}
- fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
- None
+ fn screen_by_id(&self, uuid: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
+ if self.active_screen.uuid == uuid {
+ Some(Rc::new(self.active_screen.clone()))
+ } else {
+ None
+ }
}
fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
- Default::default()
+ vec![Rc::new(self.active_screen.clone())]
}
fn open_window(
@@ -158,6 +164,7 @@ impl super::Platform for Platform {
WindowBounds::Fixed(rect) => rect.size(),
},
self.active_window.clone(),
+ Rc::new(self.active_screen.clone()),
))
}
@@ -170,6 +177,7 @@ impl super::Platform for Platform {
handle,
vec2f(24., 24.),
self.active_window.clone(),
+ Rc::new(self.active_screen.clone()),
))
}
@@ -238,8 +246,18 @@ impl super::Platform for Platform {
fn restart(&self) {}
}
-#[derive(Debug)]
-pub struct Screen;
+#[derive(Debug, Clone)]
+pub struct Screen {
+ uuid: uuid::Uuid,
+}
+
+impl Screen {
+ fn new() -> Self {
+ Self {
+ uuid: uuid::Uuid::new_v4(),
+ }
+ }
+}
impl super::Screen for Screen {
fn as_any(&self) -> &dyn Any {
@@ -255,7 +273,7 @@ impl super::Screen for Screen {
}
fn display_uuid(&self) -> Option<uuid::Uuid> {
- Some(uuid::Uuid::new_v4())
+ Some(self.uuid)
}
}
@@ -275,6 +293,7 @@ pub struct Window {
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+ screen: Rc<Screen>,
}
impl Window {
@@ -282,6 +301,7 @@ impl Window {
handle: AnyWindowHandle,
size: Vector2F,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+ screen: Rc<Screen>,
) -> Self {
Self {
handle,
@@ -299,6 +319,7 @@ impl Window {
edited: false,
pending_prompts: Default::default(),
active_window,
+ screen,
}
}
@@ -329,7 +350,7 @@ impl super::Window for Window {
}
fn screen(&self) -> Rc<dyn crate::platform::Screen> {
- Rc::new(Screen)
+ self.screen.clone()
}
fn mouse_position(&self) -> Vector2F {
@@ -11,7 +11,7 @@ mod project_tests;
mod worktree_tests;
use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope, UserId, UserStore};
+use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use copilot::Copilot;
@@ -253,13 +253,6 @@ enum ProjectClientState {
},
}
-#[derive(Clone, Debug)]
-pub struct Collaborator {
- pub peer_id: proto::PeerId,
- pub replica_id: ReplicaId,
- pub user_id: UserId,
-}
-
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
LanguageServerAdded(LanguageServerId),
@@ -982,6 +975,10 @@ impl Project {
&self.collaborators
}
+ pub fn host(&self) -> Option<&Collaborator> {
+ self.collaborators.values().find(|c| c.replica_id == 0)
+ }
+
/// Collect all worktrees, including ones that don't appear in the project panel
pub fn worktrees<'a>(
&'a self,
@@ -2231,26 +2228,62 @@ impl Project {
.get_mut(&buffer.remote_id())
.and_then(|m| m.get_mut(&language_server.server_id()))?;
let previous_snapshot = buffer_snapshots.last()?;
- let next_version = previous_snapshot.version + 1;
- let content_changes = buffer
- .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version())
- .map(|edit| {
- let edit_start = edit.new.start.0;
- let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
- let new_text = next_snapshot
- .text_for_range(edit.new.start.1..edit.new.end.1)
- .collect();
- lsp::TextDocumentContentChangeEvent {
- range: Some(lsp::Range::new(
- point_to_lsp(edit_start),
- point_to_lsp(edit_end),
- )),
+ let build_incremental_change = || {
+ buffer
+ .edits_since::<(PointUtf16, usize)>(
+ previous_snapshot.snapshot.version(),
+ )
+ .map(|edit| {
+ let edit_start = edit.new.start.0;
+ let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+ let new_text = next_snapshot
+ .text_for_range(edit.new.start.1..edit.new.end.1)
+ .collect();
+ lsp::TextDocumentContentChangeEvent {
+ range: Some(lsp::Range::new(
+ point_to_lsp(edit_start),
+ point_to_lsp(edit_end),
+ )),
+ range_length: None,
+ text: new_text,
+ }
+ })
+ .collect()
+ };
+
+ let document_sync_kind = language_server
+ .capabilities()
+ .text_document_sync
+ .as_ref()
+ .and_then(|sync| match sync {
+ lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind),
+ lsp::TextDocumentSyncCapability::Options(options) => options.change,
+ });
+
+ let content_changes: Vec<_> = match document_sync_kind {
+ Some(lsp::TextDocumentSyncKind::FULL) => {
+ vec![lsp::TextDocumentContentChangeEvent {
+ range: None,
range_length: None,
- text: new_text,
+ text: next_snapshot.text(),
+ }]
+ }
+ Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(),
+ _ => {
+ #[cfg(any(test, feature = "test-support"))]
+ {
+ build_incremental_change()
}
- })
- .collect();
+
+ #[cfg(not(any(test, feature = "test-support")))]
+ {
+ continue;
+ }
+ }
+ };
+
+ let next_version = previous_snapshot.version + 1;
buffer_snapshots.push(LspBufferSnapshot {
version: next_version,
@@ -4928,8 +4961,16 @@ impl Project {
if abs_path.ends_with("/") {
fs.create_dir(&abs_path).await?;
} else {
- fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default())
- .await?;
+ fs.create_file(
+ &abs_path,
+ op.options
+ .map(|options| fs::CreateOptions {
+ overwrite: options.overwrite.unwrap_or(false),
+ ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+ })
+ .unwrap_or_default(),
+ )
+ .await?;
}
}
@@ -4945,7 +4986,12 @@ impl Project {
fs.rename(
&source_abs_path,
&target_abs_path,
- op.options.map(Into::into).unwrap_or_default(),
+ op.options
+ .map(|options| fs::RenameOptions {
+ overwrite: options.overwrite.unwrap_or(false),
+ ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+ })
+ .unwrap_or_default(),
)
.await?;
}
@@ -4955,7 +5001,13 @@ impl Project {
.uri
.to_file_path()
.map_err(|_| anyhow!("can't convert URI to path"))?;
- let options = op.options.map(Into::into).unwrap_or_default();
+ let options = op
+ .options
+ .map(|options| fs::RemoveOptions {
+ recursive: options.recursive.unwrap_or(false),
+ ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
+ })
+ .unwrap_or_default();
if abs_path.ends_with("/") {
fs.remove_dir(&abs_path, options).await?;
} else {
@@ -8216,16 +8268,6 @@ impl Entity for Project {
}
}
-impl Collaborator {
- fn from_proto(message: proto::Collaborator) -> Result<Self> {
- Ok(Self {
- peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
- replica_id: message.replica_id as ReplicaId,
- user_id: message.user_id as UserId,
- })
- }
-}
-
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
Self {
@@ -84,6 +84,7 @@ impl Project {
terminal_settings::ActivateScript::Default => "activate",
terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish",
+ terminal_settings::ActivateScript::Nushell => "activate.nu",
};
for virtual_environment_name in settings.directories {
@@ -23,154 +23,152 @@ message Envelope {
CreateRoomResponse create_room_response = 10;
JoinRoom join_room = 11;
JoinRoomResponse join_room_response = 12;
- RejoinRoom rejoin_room = 108;
- RejoinRoomResponse rejoin_room_response = 109;
- LeaveRoom leave_room = 13;
- Call call = 14;
- IncomingCall incoming_call = 15;
- CallCanceled call_canceled = 16;
- CancelCall cancel_call = 17;
- DeclineCall decline_call = 18;
- UpdateParticipantLocation update_participant_location = 19;
- RoomUpdated room_updated = 20;
-
- ShareProject share_project = 21;
- ShareProjectResponse share_project_response = 22;
- UnshareProject unshare_project = 23;
- JoinProject join_project = 24;
- JoinProjectResponse join_project_response = 25;
- LeaveProject leave_project = 26;
- AddProjectCollaborator add_project_collaborator = 27;
- UpdateProjectCollaborator update_project_collaborator = 110;
- RemoveProjectCollaborator remove_project_collaborator = 28;
-
- GetDefinition get_definition = 29;
- GetDefinitionResponse get_definition_response = 30;
- GetTypeDefinition get_type_definition = 31;
- GetTypeDefinitionResponse get_type_definition_response = 32;
- GetReferences get_references = 33;
- GetReferencesResponse get_references_response = 34;
- GetDocumentHighlights get_document_highlights = 35;
- GetDocumentHighlightsResponse get_document_highlights_response = 36;
- GetProjectSymbols get_project_symbols = 37;
- GetProjectSymbolsResponse get_project_symbols_response = 38;
- OpenBufferForSymbol open_buffer_for_symbol = 39;
- OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
-
- UpdateProject update_project = 41;
- UpdateWorktree update_worktree = 43;
-
- CreateProjectEntry create_project_entry = 45;
- RenameProjectEntry rename_project_entry = 46;
- CopyProjectEntry copy_project_entry = 47;
- DeleteProjectEntry delete_project_entry = 48;
- ProjectEntryResponse project_entry_response = 49;
- ExpandProjectEntry expand_project_entry = 114;
- ExpandProjectEntryResponse expand_project_entry_response = 115;
-
- UpdateDiagnosticSummary update_diagnostic_summary = 50;
- StartLanguageServer start_language_server = 51;
- UpdateLanguageServer update_language_server = 52;
-
- OpenBufferById open_buffer_by_id = 53;
- OpenBufferByPath open_buffer_by_path = 54;
- OpenBufferResponse open_buffer_response = 55;
- CreateBufferForPeer create_buffer_for_peer = 56;
- UpdateBuffer update_buffer = 57;
- UpdateBufferFile update_buffer_file = 58;
- SaveBuffer save_buffer = 59;
- BufferSaved buffer_saved = 60;
- BufferReloaded buffer_reloaded = 61;
- ReloadBuffers reload_buffers = 62;
- ReloadBuffersResponse reload_buffers_response = 63;
- SynchronizeBuffers synchronize_buffers = 200;
- SynchronizeBuffersResponse synchronize_buffers_response = 201;
- FormatBuffers format_buffers = 64;
- FormatBuffersResponse format_buffers_response = 65;
- GetCompletions get_completions = 66;
- GetCompletionsResponse get_completions_response = 67;
- ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68;
- ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69;
- GetCodeActions get_code_actions = 70;
- GetCodeActionsResponse get_code_actions_response = 71;
- GetHover get_hover = 72;
- GetHoverResponse get_hover_response = 73;
- ApplyCodeAction apply_code_action = 74;
- ApplyCodeActionResponse apply_code_action_response = 75;
- PrepareRename prepare_rename = 76;
- PrepareRenameResponse prepare_rename_response = 77;
- PerformRename perform_rename = 78;
- PerformRenameResponse perform_rename_response = 79;
- SearchProject search_project = 80;
- SearchProjectResponse search_project_response = 81;
-
- UpdateContacts update_contacts = 92;
- UpdateInviteInfo update_invite_info = 93;
- ShowContacts show_contacts = 94;
-
- GetUsers get_users = 95;
- FuzzySearchUsers fuzzy_search_users = 96;
- UsersResponse users_response = 97;
- RequestContact request_contact = 98;
- RespondToContactRequest respond_to_contact_request = 99;
- RemoveContact remove_contact = 100;
-
- Follow follow = 101;
- FollowResponse follow_response = 102;
- UpdateFollowers update_followers = 103;
- Unfollow unfollow = 104;
- GetPrivateUserInfo get_private_user_info = 105;
- GetPrivateUserInfoResponse get_private_user_info_response = 106;
- UpdateDiffBase update_diff_base = 107;
-
- OnTypeFormatting on_type_formatting = 111;
- OnTypeFormattingResponse on_type_formatting_response = 112;
-
- UpdateWorktreeSettings update_worktree_settings = 113;
-
- InlayHints inlay_hints = 116;
- InlayHintsResponse inlay_hints_response = 117;
- ResolveInlayHint resolve_inlay_hint = 137;
- ResolveInlayHintResponse resolve_inlay_hint_response = 138;
- RefreshInlayHints refresh_inlay_hints = 118;
-
- CreateChannel create_channel = 119;
- CreateChannelResponse create_channel_response = 120;
- InviteChannelMember invite_channel_member = 121;
- RemoveChannelMember remove_channel_member = 122;
- RespondToChannelInvite respond_to_channel_invite = 123;
- UpdateChannels update_channels = 124;
- JoinChannel join_channel = 125;
- DeleteChannel delete_channel = 126;
- GetChannelMembers get_channel_members = 127;
- GetChannelMembersResponse get_channel_members_response = 128;
- SetChannelMemberAdmin set_channel_member_admin = 129;
- RenameChannel rename_channel = 130;
- RenameChannelResponse rename_channel_response = 154;
-
- JoinChannelBuffer join_channel_buffer = 131;
- JoinChannelBufferResponse join_channel_buffer_response = 132;
- UpdateChannelBuffer update_channel_buffer = 133;
- LeaveChannelBuffer leave_channel_buffer = 134;
- AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
- RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
- UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
- RejoinChannelBuffers rejoin_channel_buffers = 140;
- RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
-
- JoinChannelChat join_channel_chat = 142;
- JoinChannelChatResponse join_channel_chat_response = 143;
- LeaveChannelChat leave_channel_chat = 144;
- SendChannelMessage send_channel_message = 145;
- SendChannelMessageResponse send_channel_message_response = 146;
- ChannelMessageSent channel_message_sent = 147;
- GetChannelMessages get_channel_messages = 148;
- GetChannelMessagesResponse get_channel_messages_response = 149;
- RemoveChannelMessage remove_channel_message = 150;
-
- LinkChannel link_channel = 151;
- UnlinkChannel unlink_channel = 152;
- MoveChannel move_channel = 153; // Current max: 154
+ RejoinRoom rejoin_room = 13;
+ RejoinRoomResponse rejoin_room_response = 14;
+ LeaveRoom leave_room = 15;
+ Call call = 16;
+ IncomingCall incoming_call = 17;
+ CallCanceled call_canceled = 18;
+ CancelCall cancel_call = 19;
+ DeclineCall decline_call = 20;
+ UpdateParticipantLocation update_participant_location = 21;
+ RoomUpdated room_updated = 22;
+
+ ShareProject share_project = 23;
+ ShareProjectResponse share_project_response = 24;
+ UnshareProject unshare_project = 25;
+ JoinProject join_project = 26;
+ JoinProjectResponse join_project_response = 27;
+ LeaveProject leave_project = 28;
+ AddProjectCollaborator add_project_collaborator = 29;
+ UpdateProjectCollaborator update_project_collaborator = 30;
+ RemoveProjectCollaborator remove_project_collaborator = 31;
+
+ GetDefinition get_definition = 32;
+ GetDefinitionResponse get_definition_response = 33;
+ GetTypeDefinition get_type_definition = 34;
+ GetTypeDefinitionResponse get_type_definition_response = 35;
+ GetReferences get_references = 36;
+ GetReferencesResponse get_references_response = 37;
+ GetDocumentHighlights get_document_highlights = 38;
+ GetDocumentHighlightsResponse get_document_highlights_response = 39;
+ GetProjectSymbols get_project_symbols = 40;
+ GetProjectSymbolsResponse get_project_symbols_response = 41;
+ OpenBufferForSymbol open_buffer_for_symbol = 42;
+ OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
+
+ UpdateProject update_project = 44;
+ UpdateWorktree update_worktree = 45;
+
+ CreateProjectEntry create_project_entry = 46;
+ RenameProjectEntry rename_project_entry = 47;
+ CopyProjectEntry copy_project_entry = 48;
+ DeleteProjectEntry delete_project_entry = 49;
+ ProjectEntryResponse project_entry_response = 50;
+ ExpandProjectEntry expand_project_entry = 51;
+ ExpandProjectEntryResponse expand_project_entry_response = 52;
+
+ UpdateDiagnosticSummary update_diagnostic_summary = 53;
+ StartLanguageServer start_language_server = 54;
+ UpdateLanguageServer update_language_server = 55;
+
+ OpenBufferById open_buffer_by_id = 56;
+ OpenBufferByPath open_buffer_by_path = 57;
+ OpenBufferResponse open_buffer_response = 58;
+ CreateBufferForPeer create_buffer_for_peer = 59;
+ UpdateBuffer update_buffer = 60;
+ UpdateBufferFile update_buffer_file = 61;
+ SaveBuffer save_buffer = 62;
+ BufferSaved buffer_saved = 63;
+ BufferReloaded buffer_reloaded = 64;
+ ReloadBuffers reload_buffers = 65;
+ ReloadBuffersResponse reload_buffers_response = 66;
+ SynchronizeBuffers synchronize_buffers = 67;
+ SynchronizeBuffersResponse synchronize_buffers_response = 68;
+ FormatBuffers format_buffers = 69;
+ FormatBuffersResponse format_buffers_response = 70;
+ GetCompletions get_completions = 71;
+ GetCompletionsResponse get_completions_response = 72;
+ ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
+ ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
+ GetCodeActions get_code_actions = 75;
+ GetCodeActionsResponse get_code_actions_response = 76;
+ GetHover get_hover = 77;
+ GetHoverResponse get_hover_response = 78;
+ ApplyCodeAction apply_code_action = 79;
+ ApplyCodeActionResponse apply_code_action_response = 80;
+ PrepareRename prepare_rename = 81;
+ PrepareRenameResponse prepare_rename_response = 82;
+ PerformRename perform_rename = 83;
+ PerformRenameResponse perform_rename_response = 84;
+ SearchProject search_project = 85;
+ SearchProjectResponse search_project_response = 86;
+
+ UpdateContacts update_contacts = 87;
+ UpdateInviteInfo update_invite_info = 88;
+ ShowContacts show_contacts = 89;
+
+ GetUsers get_users = 90;
+ FuzzySearchUsers fuzzy_search_users = 91;
+ UsersResponse users_response = 92;
+ RequestContact request_contact = 93;
+ RespondToContactRequest respond_to_contact_request = 94;
+ RemoveContact remove_contact = 95;
+
+ Follow follow = 96;
+ FollowResponse follow_response = 97;
+ UpdateFollowers update_followers = 98;
+ Unfollow unfollow = 99;
+ GetPrivateUserInfo get_private_user_info = 100;
+ GetPrivateUserInfoResponse get_private_user_info_response = 101;
+ UpdateDiffBase update_diff_base = 102;
+
+ OnTypeFormatting on_type_formatting = 103;
+ OnTypeFormattingResponse on_type_formatting_response = 104;
+
+ UpdateWorktreeSettings update_worktree_settings = 105;
+
+ InlayHints inlay_hints = 106;
+ InlayHintsResponse inlay_hints_response = 107;
+ ResolveInlayHint resolve_inlay_hint = 108;
+ ResolveInlayHintResponse resolve_inlay_hint_response = 109;
+ RefreshInlayHints refresh_inlay_hints = 110;
+
+ CreateChannel create_channel = 111;
+ CreateChannelResponse create_channel_response = 112;
+ InviteChannelMember invite_channel_member = 113;
+ RemoveChannelMember remove_channel_member = 114;
+ RespondToChannelInvite respond_to_channel_invite = 115;
+ UpdateChannels update_channels = 116;
+ JoinChannel join_channel = 117;
+ DeleteChannel delete_channel = 118;
+ GetChannelMembers get_channel_members = 119;
+ GetChannelMembersResponse get_channel_members_response = 120;
+ SetChannelMemberAdmin set_channel_member_admin = 121;
+ RenameChannel rename_channel = 122;
+ RenameChannelResponse rename_channel_response = 123;
+
+ JoinChannelBuffer join_channel_buffer = 124;
+ JoinChannelBufferResponse join_channel_buffer_response = 125;
+ UpdateChannelBuffer update_channel_buffer = 126;
+ LeaveChannelBuffer leave_channel_buffer = 127;
+ UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
+ RejoinChannelBuffers rejoin_channel_buffers = 129;
+ RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
+
+ JoinChannelChat join_channel_chat = 131;
+ JoinChannelChatResponse join_channel_chat_response = 132;
+ LeaveChannelChat leave_channel_chat = 133;
+ SendChannelMessage send_channel_message = 134;
+ SendChannelMessageResponse send_channel_message_response = 135;
+ ChannelMessageSent channel_message_sent = 136;
+ GetChannelMessages get_channel_messages = 137;
+ GetChannelMessagesResponse get_channel_messages_response = 138;
+ RemoveChannelMessage remove_channel_message = 139;
+
+ LinkChannel link_channel = 140;
+ UnlinkChannel unlink_channel = 141;
+ MoveChannel move_channel = 142;
}
}
@@ -258,6 +256,7 @@ message Participant {
PeerId peer_id = 2;
repeated ParticipantProject projects = 3;
ParticipantLocation location = 4;
+ uint32 participant_index = 5;
}
message PendingParticipant {
@@ -440,20 +439,9 @@ message RemoveProjectCollaborator {
PeerId peer_id = 2;
}
-message AddChannelBufferCollaborator {
+message UpdateChannelBufferCollaborators {
uint64 channel_id = 1;
- Collaborator collaborator = 2;
-}
-
-message RemoveChannelBufferCollaborator {
- uint64 channel_id = 1;
- PeerId peer_id = 2;
-}
-
-message UpdateChannelBufferCollaborator {
- uint64 channel_id = 1;
- PeerId old_peer_id = 2;
- PeerId new_peer_id = 3;
+ repeated Collaborator collaborators = 2;
}
message GetDefinition {
@@ -1213,8 +1201,9 @@ message UpdateDiagnostics {
}
message Follow {
- uint64 project_id = 1;
- PeerId leader_id = 2;
+ uint64 room_id = 1;
+ optional uint64 project_id = 2;
+ PeerId leader_id = 3;
}
message FollowResponse {
@@ -1223,18 +1212,20 @@ message FollowResponse {
}
message UpdateFollowers {
- uint64 project_id = 1;
- repeated PeerId follower_ids = 2;
+ uint64 room_id = 1;
+ optional uint64 project_id = 2;
+ repeated PeerId follower_ids = 3;
oneof variant {
- UpdateActiveView update_active_view = 3;
- View create_view = 4;
- UpdateView update_view = 5;
+ UpdateActiveView update_active_view = 4;
+ View create_view = 5;
+ UpdateView update_view = 6;
}
}
message Unfollow {
- uint64 project_id = 1;
- PeerId leader_id = 2;
+ uint64 room_id = 1;
+ optional uint64 project_id = 2;
+ PeerId leader_id = 3;
}
message GetPrivateUserInfo {}
@@ -270,9 +270,7 @@ messages!(
(JoinChannelBufferResponse, Foreground),
(LeaveChannelBuffer, Background),
(UpdateChannelBuffer, Foreground),
- (RemoveChannelBufferCollaborator, Foreground),
- (AddChannelBufferCollaborator, Foreground),
- (UpdateChannelBufferCollaborator, Foreground),
+ (UpdateChannelBufferCollaborators, Foreground),
);
request_messages!(
@@ -364,7 +362,6 @@ entity_messages!(
CreateProjectEntry,
DeleteProjectEntry,
ExpandProjectEntry,
- Follow,
FormatBuffers,
GetCodeActions,
GetCompletions,
@@ -392,12 +389,10 @@ entity_messages!(
SearchProject,
StartLanguageServer,
SynchronizeBuffers,
- Unfollow,
UnshareProject,
UpdateBuffer,
UpdateBufferFile,
UpdateDiagnosticSummary,
- UpdateFollowers,
UpdateLanguageServer,
UpdateProject,
UpdateProjectCollaborator,
@@ -410,10 +405,8 @@ entity_messages!(
channel_id,
ChannelMessageSent,
UpdateChannelBuffer,
- RemoveChannelBufferCollaborator,
RemoveChannelMessage,
- AddChannelBufferCollaborator,
- UpdateChannelBufferCollaborator
+ UpdateChannelBufferCollaborators
);
const KIB: usize = 1024;
@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 63;
+pub const PROTOCOL_VERSION: u32 = 64;
@@ -61,8 +61,9 @@ const CODE_CONTEXT_TEMPLATE: &str =
const ENTIRE_FILE_TEMPLATE: &str =
"The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
-pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
- &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
+ "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
+];
pub struct CodeContextRetriever {
pub parser: Parser,
@@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() {
todo!();
}
}
+
+ #[derive(Clone)]
+ struct D {
+ name: String
+ }
"
.unindent();
@@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() {
.unindent(),
text.find("fn function_2").unwrap(),
),
+ (
+ "
+ #[derive(Clone)]
+ struct D {
+ name: String
+ }"
+ .unindent(),
+ text.find("struct D").unwrap(),
+ ),
],
);
}
@@ -1422,6 +1436,9 @@ fn rust_lang() -> Arc<Language> {
name: (_) @name)
] @item
)
+
+ (attribute_item) @collapse
+ (use_declaration) @collapse
"#,
)
.unwrap(),
@@ -12,6 +12,8 @@ path = "src/storybook.rs"
anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] }
chrono = "0.4"
+fs = { path = "../fs" }
+futures.workspace = true
gpui2 = { path = "../gpui2" }
itertools = "0.11.0"
log.workspace = true
@@ -1,5 +1,8 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+
use ui::prelude::*;
-use ui::Breadcrumb;
+use ui::{Breadcrumb, HighlightedText, Symbol};
use crate::story::Story;
@@ -8,9 +11,35 @@ pub struct BreadcrumbStory {}
impl BreadcrumbStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx);
+
Story::container(cx)
.child(Story::title_for::<_, Breadcrumb>(cx))
.child(Story::label(cx, "Default"))
- .child(Breadcrumb::new())
+ .child(Breadcrumb::new(
+ PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+ vec![
+ Symbol(vec![
+ HighlightedText {
+ text: "impl ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "BreadcrumbStory".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ]),
+ Symbol(vec![
+ HighlightedText {
+ text: "fn ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "render".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ]),
+ ],
+ ))
}
}
@@ -12,8 +12,10 @@ pub struct BufferStory {}
impl BufferStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx);
+
Story::container(cx)
- .child(Story::title_for::<_, Buffer<V>>(cx))
+ .child(Story::title_for::<_, Buffer>(cx))
.child(Story::label(cx, "Default"))
.child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
.child(Story::label(cx, "Hello World (Rust)"))
@@ -21,14 +23,14 @@ impl BufferStory {
div()
.w(rems(64.))
.h_96()
- .child(hello_world_rust_buffer_example(cx)),
+ .child(hello_world_rust_buffer_example(&theme)),
)
.child(Story::label(cx, "Hello World (Rust) with Status"))
.child(
div()
.w(rems(64.))
.h_96()
- .child(hello_world_rust_buffer_with_status_example(cx)),
+ .child(hello_world_rust_buffer_with_status_example(&theme)),
)
}
}
@@ -1,6 +1,6 @@
use chrono::DateTime;
use ui::prelude::*;
-use ui::{ChatMessage, ChatPanel};
+use ui::{ChatMessage, ChatPanel, Panel};
use crate::story::Story;
@@ -12,23 +12,35 @@ impl ChatPanelStory {
Story::container(cx)
.child(Story::title_for::<_, ChatPanel<V>>(cx))
.child(Story::label(cx, "Default"))
- .child(ChatPanel::new(ScrollState::default()))
+ .child(Panel::new(
+ ScrollState::default(),
+ |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
+ Box::new(()),
+ ))
.child(Story::label(cx, "With Mesages"))
- .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ]))
+ .child(Panel::new(
+ ScrollState::default(),
+ |_, _| {
+ vec![ChatPanel::new(ScrollState::default())
+ .with_messages(vec![
+ ChatMessage::new(
+ "osiewicz".to_string(),
+ "is this thing on?".to_string(),
+ DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+ .unwrap()
+ .naive_local(),
+ ),
+ ChatMessage::new(
+ "maxdeviant".to_string(),
+ "Reading you loud and clear!".to_string(),
+ DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+ .unwrap()
+ .naive_local(),
+ ),
+ ])
+ .into_any()]
+ },
+ Box::new(()),
+ ))
}
}
@@ -11,7 +11,7 @@ impl FacepileStory {
let players = static_players();
Story::container(cx)
- .child(Story::title_for::<_, ui::Facepile>(cx))
+ .child(Story::title_for::<_, Facepile>(cx))
.child(Story::label(cx, "Default"))
.child(
div()
@@ -14,9 +14,10 @@ impl PanelStory {
.child(Panel::new(
ScrollState::default(),
|_, _| {
- (0..100)
- .map(|ix| Label::new(format!("Item {}", ix + 1)).into_any())
- .collect()
+ vec![div()
+ .overflow_y_scroll(ScrollState::default())
+ .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
+ .into_any()]
},
Box::new(()),
))
@@ -1,5 +1,5 @@
use ui::prelude::*;
-use ui::ProjectPanel;
+use ui::{Panel, ProjectPanel};
use crate::story::Story;
@@ -11,6 +11,10 @@ impl ProjectPanelStory {
Story::container(cx)
.child(Story::title_for::<_, ProjectPanel<V>>(cx))
.child(Story::label(cx, "Default"))
- .child(ProjectPanel::new(ScrollState::default()))
+ .child(Panel::new(
+ ScrollState::default(),
+ |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+ Box::new(()),
+ ))
}
}
@@ -1,5 +1,5 @@
use ui::prelude::*;
-use ui::TabBar;
+use ui::{Tab, TabBar};
use crate::story::Story;
@@ -11,6 +11,36 @@ impl TabBarStory {
Story::container(cx)
.child(Story::title_for::<_, TabBar<V>>(cx))
.child(Story::label(cx, "Default"))
- .child(TabBar::new(ScrollState::default()))
+ .child(TabBar::new(vec![
+ Tab::new()
+ .title("Cargo.toml".to_string())
+ .current(false)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("Channels Panel".to_string())
+ .current(false),
+ Tab::new()
+ .title("channels_panel.rs".to_string())
+ .current(true)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("workspace.rs".to_string())
+ .current(false)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("icon_button.rs".to_string())
+ .current(false),
+ Tab::new()
+ .title("storybook.rs".to_string())
+ .current(false)
+ .git_status(GitStatus::Created),
+ Tab::new().title("theme.rs".to_string()).current(false),
+ Tab::new()
+ .title("theme_registry.rs".to_string())
+ .current(false),
+ Tab::new()
+ .title("styleable_helpers.rs".to_string())
+ .current(false),
+ ]))
}
}
@@ -1,5 +1,9 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+
use ui::prelude::*;
-use ui::Toolbar;
+use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar};
use crate::story::Story;
@@ -8,9 +12,59 @@ pub struct ToolbarStory {}
impl ToolbarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx);
+
+ struct LeftItemsPayload {
+ pub theme: Arc<Theme>,
+ }
+
Story::container(cx)
- .child(Story::title_for::<_, Toolbar>(cx))
+ .child(Story::title_for::<_, Toolbar<V>>(cx))
.child(Story::label(cx, "Default"))
- .child(Toolbar::new())
+ .child(Toolbar::new(
+ |_, payload| {
+ let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+ let theme = payload.theme.clone();
+
+ vec![Breadcrumb::new(
+ PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+ vec![
+ Symbol(vec![
+ HighlightedText {
+ text: "impl ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "ToolbarStory".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ]),
+ Symbol(vec![
+ HighlightedText {
+ text: "fn ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "render".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ]),
+ ],
+ )
+ .into_any()]
+ },
+ Box::new(LeftItemsPayload {
+ theme: theme.clone(),
+ }),
+ |_, _| {
+ vec![
+ IconButton::new(Icon::InlayHint).into_any(),
+ IconButton::new(Icon::MagnifyingGlass).into_any(),
+ IconButton::new(Icon::MagicWand).into_any(),
+ ]
+ },
+ Box::new(()),
+ ))
}
}
@@ -9,7 +9,7 @@ pub struct AvatarStory {}
impl AvatarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
- .child(Story::title_for::<_, ui::Avatar>(cx))
+ .child(Story::title_for::<_, Avatar>(cx))
.child(Story::label(cx, "Default"))
.child(Avatar::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
@@ -12,7 +12,7 @@ impl IconStory {
let icons = Icon::iter();
Story::container(cx)
- .child(Story::title_for::<_, ui::IconElement>(cx))
+ .child(Story::title_for::<_, IconElement>(cx))
.child(Story::label(cx, "All Icons"))
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
}
@@ -19,5 +19,8 @@ impl KitchenSinkStory {
.child(div().flex().flex_col().children_any(element_stories))
.child(Story::label(cx, "Components"))
.child(div().flex().flex_col().children_any(component_stories))
+ // Add a bit of space at the bottom of the kitchen sink so elements
+ // don't end up squished right up against the bottom of the screen.
+ .child(div().p_4())
}
}
@@ -4,7 +4,7 @@ mod stories;
mod story;
mod story_selector;
-use std::sync::Arc;
+use std::{process::Command, sync::Arc};
use ::theme as legacy_theme;
use clap::Parser;
@@ -38,11 +38,44 @@ struct Args {
theme: Option<String>,
}
+async fn watch_zed_changes(fs: Arc<dyn fs::Fs>) -> Option<()> {
+ if std::env::var("ZED_HOT_RELOAD").is_err() {
+ return None;
+ }
+ use futures::StreamExt;
+ let mut events = fs
+ .watch(".".as_ref(), std::time::Duration::from_millis(100))
+ .await;
+ let mut current_child: Option<std::process::Child> = None;
+ while let Some(events) = events.next().await {
+ if !events.iter().any(|event| {
+ event
+ .path
+ .to_str()
+ .map(|path| path.contains("/crates/"))
+ .unwrap_or_default()
+ }) {
+ continue;
+ }
+ let child = current_child.take().map(|mut child| child.kill());
+ log::info!("Storybook changed, rebuilding...");
+ current_child = Some(
+ Command::new("cargo")
+ .args(["run", "-p", "storybook"])
+ .spawn()
+ .ok()?,
+ );
+ }
+ Some(())
+}
+
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
let args = Args::parse();
+ let fs = Arc::new(fs::RealFs);
+
gpui2::App::new(Assets).unwrap().run(move |cx| {
let mut store = SettingsStore::default();
store
@@ -63,6 +96,10 @@ fn main() {
})
.and_then(|theme_name| theme_registry.get(&theme_name).ok());
+ cx.spawn(|_| async move {
+ watch_zed_changes(fs).await;
+ })
+ .detach();
cx.add_window(
gpui2::WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),
@@ -69,6 +69,7 @@ pub enum ActivateScript {
Default,
Csh,
Fish,
+ Nushell,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -131,6 +131,7 @@ pub struct Titlebar {
pub menu: TitlebarMenu,
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
+ pub project_host: Interactive<ContainedText>,
pub item_spacing: f32,
pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon,
@@ -1065,13 +1066,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
}
impl Editor {
- pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
- let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
- if style_ix == 0 {
- &self.selection
- } else {
- &self.guest_selections[style_ix - 1]
+ pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle {
+ if self.guest_selections.is_empty() {
+ return SelectionStyle::default();
}
+ let style_ix = participant_index as usize % self.guest_selections.len();
+ self.guest_selections[style_ix]
}
}
@@ -13,3 +13,4 @@ settings = { path = "../settings" }
smallvec.workspace = true
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" }
+rand = "0.8"
@@ -5,7 +5,7 @@ mod chat_panel;
mod collab_panel;
mod command_palette;
mod context_menu;
-mod editor;
+mod editor_pane;
mod facepile;
mod icon_button;
mod keybinding;
@@ -31,7 +31,7 @@ pub use chat_panel::*;
pub use collab_panel::*;
pub use command_palette::*;
pub use context_menu::*;
-pub use editor::*;
+pub use editor_pane::*;
pub use facepile::*;
pub use icon_button::*;
pub use keybinding::*;
@@ -1,17 +1,35 @@
-use crate::prelude::*;
+use std::path::PathBuf;
+
+use gpui2::elements::div::Div;
+
use crate::{h_stack, theme};
+use crate::{prelude::*, HighlightedText};
+
+#[derive(Clone)]
+pub struct Symbol(pub Vec<HighlightedText>);
#[derive(Element)]
-pub struct Breadcrumb {}
+pub struct Breadcrumb {
+ path: PathBuf,
+ symbols: Vec<Symbol>,
+}
impl Breadcrumb {
- pub fn new() -> Self {
- Self {}
+ pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
+ Self { path, symbols }
+ }
+
+ fn render_separator<V: 'static>(&self, theme: &Theme) -> Div<V> {
+ div()
+ .child(" › ")
+ .text_color(HighlightColor::Default.hsla(theme))
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
+ let symbols_len = self.symbols.len();
+
h_stack()
.px_1()
// TODO: Read font from theme (or settings?).
@@ -21,11 +39,33 @@ impl Breadcrumb {
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
- // TODO: Replace hardcoded breadcrumbs.
- .child("crates/ui/src/components/toolbar.rs")
- .child(" › ")
- .child("impl Breadcrumb")
- .child(" › ")
- .child("fn render")
+ .child(self.path.clone().to_str().unwrap().to_string())
+ .child(if !self.symbols.is_empty() {
+ self.render_separator(&theme)
+ } else {
+ div()
+ })
+ .child(
+ div().flex().children(
+ self.symbols
+ .iter()
+ .enumerate()
+ // TODO: Could use something like `intersperse` here instead.
+ .flat_map(|(ix, symbol)| {
+ let mut items =
+ vec![div().flex().children(symbol.0.iter().map(|segment| {
+ div().child(segment.text.clone()).text_color(segment.color)
+ }))];
+
+ let is_last_segment = ix == symbols_len - 1;
+ if !is_last_segment {
+ items.push(self.render_separator(&theme));
+ }
+
+ items
+ })
+ .collect::<Vec<_>>(),
+ ),
+ )
}
}
@@ -1,5 +1,3 @@
-use std::marker::PhantomData;
-
use gpui2::{Hsla, WindowContext};
use crate::prelude::*;
@@ -33,6 +31,7 @@ pub struct BufferRow {
pub show_line_number: bool,
}
+#[derive(Clone)]
pub struct BufferRows {
pub show_line_numbers: bool,
pub rows: Vec<BufferRow>,
@@ -108,9 +107,8 @@ impl BufferRow {
}
}
-#[derive(Element)]
-pub struct Buffer<V: 'static> {
- view_type: PhantomData<V>,
+#[derive(Element, Clone)]
+pub struct Buffer {
scroll_state: ScrollState,
rows: Option<BufferRows>,
readonly: bool,
@@ -119,10 +117,9 @@ pub struct Buffer<V: 'static> {
path: Option<String>,
}
-impl<V: 'static> Buffer<V> {
+impl Buffer {
pub fn new() -> Self {
Self {
- view_type: PhantomData,
scroll_state: ScrollState::default(),
rows: Some(BufferRows::default()),
readonly: false,
@@ -161,7 +158,7 @@ impl<V: 'static> Buffer<V> {
self
}
- fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
+ fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
let theme = theme(cx);
let system_color = SystemColor::new();
@@ -172,28 +169,35 @@ impl<V: 'static> Buffer<V> {
};
let line_number_color = if row.current {
- HighlightColor::Default.hsla(cx)
+ HighlightColor::Default.hsla(&theme)
} else {
- HighlightColor::Comment.hsla(cx)
+ HighlightColor::Comment.hsla(&theme)
};
h_stack()
.fill(line_background)
+ .w_full()
.gap_2()
- .px_2()
- .child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| {
- div().child(IconElement::new(Icon::Bolt))
- }))
+ .px_1()
+ .child(
+ h_stack()
+ .w_4()
+ .h_full()
+ .px_0p5()
+ .when(row.code_action, |c| {
+ div().child(IconElement::new(Icon::Bolt))
+ }),
+ )
.when(row.show_line_number, |this| {
this.child(
- h_stack().justify_end().px_1().w_4().child(
+ h_stack().justify_end().px_0p5().w_3().child(
div()
.text_color(line_number_color)
.child(row.line_number.to_string()),
),
)
})
- .child(div().mx_1().w_1().h_full().fill(row.status.hsla(cx)))
+ .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx)))
.children(row.line.map(|line| {
div()
.flex()
@@ -205,7 +209,7 @@ impl<V: 'static> Buffer<V> {
}))
}
- fn render_rows(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
+ fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
match &self.rows {
Some(rows) => rows
.rows
@@ -216,7 +220,7 @@ impl<V: 'static> Buffer<V> {
}
}
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let rows = self.render_rows(cx);
v_stack()
@@ -4,13 +4,12 @@ use chrono::NaiveDateTime;
use crate::prelude::*;
use crate::theme::theme;
-use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide};
+use crate::{Icon, IconButton, Input, Label, LabelColor};
#[derive(Element)]
pub struct ChatPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
- current_side: PanelSide,
messages: Vec<ChatMessage>,
}
@@ -19,16 +18,10 @@ impl<V: 'static> ChatPanel<V> {
Self {
view_type: PhantomData,
scroll_state,
- current_side: PanelSide::default(),
messages: Vec::new(),
}
}
- pub fn side(mut self, side: PanelSide) -> Self {
- self.current_side = side;
- self
- }
-
pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
self.messages = messages;
self
@@ -37,38 +30,33 @@ impl<V: 'static> ChatPanel<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
- struct PanelPayload {
- pub scroll_state: ScrollState,
- pub messages: Vec<ChatMessage>,
- }
-
- Panel::new(
- self.scroll_state.clone(),
- |_, payload| {
- let payload = payload.downcast_ref::<PanelPayload>().unwrap();
-
- vec![div()
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .h_full()
+ .px_2()
+ .gap_2()
+ // Header
+ .child(
+ div()
.flex()
- .flex_col()
- .h_full()
- .px_2()
- .gap_2()
- // Header
+ .justify_between()
+ .py_2()
+ .child(div().flex().child(Label::new("#design")))
.child(
div()
.flex()
- .justify_between()
- .gap_2()
- .child(div().flex().child(Label::new("#design")))
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new(Icon::File))
- .child(IconButton::new(Icon::AudioOn)),
- ),
- )
+ .items_center()
+ .gap_px()
+ .child(IconButton::new(Icon::File))
+ .child(IconButton::new(Icon::AudioOn)),
+ ),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
// Chat Body
.child(
div()
@@ -76,19 +64,12 @@ impl<V: 'static> ChatPanel<V> {
.flex()
.flex_col()
.gap_3()
- .overflow_y_scroll(payload.scroll_state.clone())
- .children(payload.messages.clone()),
+ .overflow_y_scroll(self.scroll_state.clone())
+ .children(self.messages.clone()),
)
// Composer
- .child(div().flex().gap_2().child(Input::new("Message #design")))
- .into_any()]
- },
- Box::new(PanelPayload {
- scroll_state: self.scroll_state.clone(),
- messages: self.messages.clone(),
- }),
- )
- .side(self.current_side)
+ .child(div().flex().my_2().child(Input::new("Message #design"))),
+ )
}
}
@@ -1,25 +0,0 @@
-use std::marker::PhantomData;
-
-use crate::prelude::*;
-use crate::{Buffer, Toolbar};
-
-#[derive(Element)]
-struct Editor<V: 'static> {
- view_type: PhantomData<V>,
- toolbar: Toolbar,
- buffer: Buffer<V>,
-}
-
-impl<V: 'static> Editor<V> {
- pub fn new(toolbar: Toolbar, buffer: Buffer<V>) -> Self {
- Self {
- view_type: PhantomData,
- toolbar,
- buffer,
- }
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- div().child(self.toolbar.clone())
- }
-}
@@ -0,0 +1,60 @@
+use std::marker::PhantomData;
+use std::path::PathBuf;
+
+use crate::prelude::*;
+use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar};
+
+pub struct Editor {
+ pub tabs: Vec<Tab>,
+ pub path: PathBuf,
+ pub symbols: Vec<Symbol>,
+ pub buffer: Buffer,
+}
+
+#[derive(Element)]
+pub struct EditorPane<V: 'static> {
+ view_type: PhantomData<V>,
+ editor: Editor,
+}
+
+impl<V: 'static> EditorPane<V> {
+ pub fn new(editor: Editor) -> Self {
+ Self {
+ view_type: PhantomData,
+ editor,
+ }
+ }
+
+ fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ struct LeftItemsPayload {
+ path: PathBuf,
+ symbols: Vec<Symbol>,
+ }
+
+ v_stack()
+ .w_full()
+ .h_full()
+ .flex_1()
+ .child(TabBar::new(self.editor.tabs.clone()))
+ .child(Toolbar::new(
+ |_, payload| {
+ let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+ vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()]
+ },
+ Box::new(LeftItemsPayload {
+ path: self.editor.path.clone(),
+ symbols: self.editor.symbols.clone(),
+ }),
+ |_, _| {
+ vec![
+ IconButton::new(Icon::InlayHint).into_any(),
+ IconButton::new(Icon::MagnifyingGlass).into_any(),
+ IconButton::new(Icon::MagicWand).into_any(),
+ ]
+ },
+ Box::new(()),
+ ))
+ .child(self.editor.buffer.clone())
+ }
+}
@@ -105,16 +105,12 @@ impl<V: 'static> Panel<V> {
let theme = theme(cx);
let panel_base;
- let current_width = if let Some(width) = self.width {
- width
- } else {
- self.initial_width
- };
+ let current_width = self.width.unwrap_or(self.initial_width);
match self.current_side {
PanelSide::Left => {
panel_base = v_stack()
- .overflow_y_scroll(self.scroll_state.clone())
+ .flex_initial()
.h_full()
.w(current_width)
.fill(theme.middle.base.default.background)
@@ -123,20 +119,20 @@ impl<V: 'static> Panel<V> {
}
PanelSide::Right => {
panel_base = v_stack()
- .overflow_y_scroll(self.scroll_state.clone())
+ .flex_initial()
.h_full()
.w(current_width)
.fill(theme.middle.base.default.background)
- .border_r()
+ .border_l()
.border_color(theme.middle.base.default.border);
}
PanelSide::Bottom => {
panel_base = v_stack()
- .overflow_y_scroll(self.scroll_state.clone())
+ .flex_initial()
.w_full()
.h(current_width)
.fill(theme.middle.base.default.background)
- .border_r()
+ .border_t()
.border_color(theme.middle.base.default.border);
}
}
@@ -38,9 +38,8 @@ impl PlayerStack {
div().flex().justify_center().w_full().child(
div()
.w_4()
- .h_1()
- .rounded_bl_sm()
- .rounded_br_sm()
+ .h_0p5()
+ .rounded_sm()
.fill(player.cursor_color(cx)),
),
)
@@ -50,7 +49,7 @@ impl PlayerStack {
.items_center()
.justify_center()
.h_6()
- .px_1()
+ .pl_1()
.rounded_lg()
.fill(if followers.is_none() {
system_color.transparent
@@ -59,7 +58,7 @@ impl PlayerStack {
})
.child(Avatar::new(player.avatar_src().to_string()))
.children(followers.map(|followers| {
- div().neg_mr_1().child(Facepile::new(followers.into_iter()))
+ div().neg_ml_2().child(Facepile::new(followers.into_iter()))
})),
)
}
@@ -1,17 +1,15 @@
use std::marker::PhantomData;
-use std::sync::Arc;
use crate::prelude::*;
use crate::{
static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
- ListHeader, Panel, PanelSide, Theme,
+ ListHeader,
};
#[derive(Element)]
pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
- current_side: PanelSide,
}
impl<V: 'static> ProjectPanel<V> {
@@ -19,69 +17,42 @@ impl<V: 'static> ProjectPanel<V> {
Self {
view_type: PhantomData,
scroll_state,
- current_side: PanelSide::default(),
}
}
- pub fn side(mut self, side: PanelSide) -> Self {
- self.current_side = side;
- self
- }
-
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- struct PanelPayload {
- pub theme: Arc<Theme>,
- pub scroll_state: ScrollState,
- }
-
- Panel::new(
- self.scroll_state.clone(),
- |_, payload| {
- let payload = payload.downcast_ref::<PanelPayload>().unwrap();
-
- let theme = payload.theme.clone();
-
- vec![div()
+ let theme = theme(cx);
+
+ div()
+ .flex()
+ .flex_col()
+ .w_full()
+ .h_full()
+ .px_2()
+ .fill(theme.middle.base.default.background)
+ .child(
+ div()
+ .w_56()
.flex()
.flex_col()
- .w_56()
- .h_full()
- .px_2()
- .fill(theme.middle.base.default.background)
+ .overflow_y_scroll(ScrollState::default())
.child(
- div()
- .w_56()
- .flex()
- .flex_col()
- .overflow_y_scroll(payload.scroll_state.clone())
- .child(
- List::new(static_project_panel_single_items())
- .header(
- ListHeader::new("FILES").set_toggle(ToggleState::Toggled),
- )
- .empty_message("No files in directory")
- .set_toggle(ToggleState::Toggled),
- )
- .child(
- List::new(static_project_panel_project_items())
- .header(
- ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled),
- )
- .empty_message("No folders in directory")
- .set_toggle(ToggleState::Toggled),
- ),
+ List::new(static_project_panel_single_items())
+ .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
+ .empty_message("No files in directory")
+ .set_toggle(ToggleState::Toggled),
)
.child(
- Input::new("Find something...")
- .value("buffe".to_string())
- .state(InteractionState::Focused),
- )
- .into_any()]
- },
- Box::new(PanelPayload {
- theme: theme(cx),
- scroll_state: self.scroll_state.clone(),
- }),
- )
+ List::new(static_project_panel_project_items())
+ .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
+ .empty_message("No folders in directory")
+ .set_toggle(ToggleState::Toggled),
+ ),
+ )
+ .child(
+ Input::new("Find something...")
+ .value("buffe".to_string())
+ .state(InteractionState::Focused),
+ )
}
}
@@ -1,7 +1,7 @@
use crate::prelude::*;
use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
-#[derive(Element)]
+#[derive(Element, Clone)]
pub struct Tab {
title: String,
icon: Option<Icon>,
@@ -7,20 +7,27 @@ use crate::{theme, Icon, IconButton, Tab};
pub struct TabBar<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
+ tabs: Vec<Tab>,
}
impl<V: 'static> TabBar<V> {
- pub fn new(scroll_state: ScrollState) -> Self {
+ pub fn new(tabs: Vec<Tab>) -> Self {
Self {
view_type: PhantomData,
- scroll_state,
+ scroll_state: ScrollState::default(),
+ tabs,
}
}
+ pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+ self.scroll_state = scroll_state;
+ }
+
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let can_navigate_back = true;
let can_navigate_forward = false;
+
div()
.w_full()
.flex()
@@ -54,51 +61,7 @@ impl<V: 'static> TabBar<V> {
div()
.flex()
.overflow_x_scroll(self.scroll_state.clone())
- .child(
- Tab::new()
- .title("Cargo.toml".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- )
- .child(
- Tab::new()
- .title("Channels Panel".to_string())
- .current(false),
- )
- .child(
- Tab::new()
- .title("channels_panel.rs".to_string())
- .current(true)
- .git_status(GitStatus::Modified),
- )
- .child(
- Tab::new()
- .title("workspace.rs".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- )
- .child(
- Tab::new()
- .title("icon_button.rs".to_string())
- .current(false),
- )
- .child(
- Tab::new()
- .title("storybook.rs".to_string())
- .current(false)
- .git_status(GitStatus::Created),
- )
- .child(Tab::new().title("theme.rs".to_string()).current(false))
- .child(
- Tab::new()
- .title("theme_registry.rs".to_string())
- .current(false),
- )
- .child(
- Tab::new()
- .title("styleable_helpers.rs".to_string())
- .current(false),
- ),
+ .children(self.tabs.clone()),
),
)
// Right Side
@@ -1,3 +1,5 @@
+use std::sync::Arc;
+
use gpui2::geometry::{relative, rems, Size};
use crate::prelude::*;
@@ -20,6 +22,7 @@ impl Terminal {
div()
.flex()
.flex_col()
+ .w_full()
.child(
// Terminal Tabs.
div()
@@ -70,8 +73,12 @@ impl Terminal {
width: relative(1.).into(),
height: rems(36.).into(),
},
- |_, _| vec![],
- Box::new(()),
+ |_, payload| {
+ let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+ vec![crate::static_data::terminal_buffer(&theme).into_any()]
+ },
+ Box::new(theme),
))
}
}
@@ -2,16 +2,24 @@ use std::marker::PhantomData;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
-use crate::prelude::*;
+use crate::{prelude::*, PlayerWithCallStatus};
use crate::{
- static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor,
- PlayerStack, ToolDivider, TrafficLights,
+ theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights,
};
+#[derive(Clone)]
+pub struct Livestream {
+ pub players: Vec<PlayerWithCallStatus>,
+ pub channel: Option<String>, // projects
+ // windows
+}
+
#[derive(Element)]
pub struct TitleBar<V: 'static> {
view_type: PhantomData<V>,
+ /// If the window is active from the OS's perspective.
is_active: Arc<AtomicBool>,
+ livestream: Option<Livestream>,
}
impl<V: 'static> TitleBar<V> {
@@ -28,14 +36,24 @@ impl<V: 'static> TitleBar<V> {
Self {
view_type: PhantomData,
is_active,
+ livestream: None,
}
}
+ pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
+ self.livestream = livestream;
+ self
+ }
+
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let has_focus = cx.window_is_active();
- let player_list = static_players_with_call_status().into_iter();
+ let player_list = if let Some(livestream) = &self.livestream {
+ livestream.players.clone().into_iter()
+ } else {
+ vec![].into_iter()
+ };
div()
.flex()
@@ -61,7 +79,8 @@ impl<V: 'static> TitleBar<V> {
.child(Button::new("zed"))
.child(Button::new("nate/gpui2-ui-components")),
)
- .children(player_list.map(|p| PlayerStack::new(p))),
+ .children(player_list.map(|p| PlayerStack::new(p)))
+ .child(IconButton::new(Icon::Plus)),
)
.child(
div()
@@ -1,33 +1,49 @@
use crate::prelude::*;
-use crate::{theme, Breadcrumb, Icon, IconButton};
+use crate::theme;
#[derive(Clone)]
pub struct ToolbarItem {}
-#[derive(Element, Clone)]
-pub struct Toolbar {
- items: Vec<ToolbarItem>,
+#[derive(Element)]
+pub struct Toolbar<V: 'static> {
+ left_items: HackyChildren<V>,
+ left_items_payload: HackyChildrenPayload,
+ right_items: HackyChildren<V>,
+ right_items_payload: HackyChildrenPayload,
}
-impl Toolbar {
- pub fn new() -> Self {
- Self { items: Vec::new() }
+impl<V: 'static> Toolbar<V> {
+ pub fn new(
+ left_items: HackyChildren<V>,
+ left_items_payload: HackyChildrenPayload,
+ right_items: HackyChildren<V>,
+ right_items_payload: HackyChildrenPayload,
+ ) -> Self {
+ Self {
+ left_items,
+ left_items_payload,
+ right_items,
+ right_items_payload,
+ }
}
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
+ .fill(theme.highest.base.default.background)
.p_2()
.flex()
.justify_between()
- .child(Breadcrumb::new())
.child(
div()
.flex()
- .child(IconButton::new(Icon::InlayHint))
- .child(IconButton::new(Icon::MagnifyingGlass))
- .child(IconButton::new(Icon::MagicWand)),
+ .children_any((self.left_items)(cx, self.left_items_payload.as_ref())),
+ )
+ .child(
+ div()
+ .flex()
+ .children_any((self.right_items)(cx, self.right_items_payload.as_ref())),
)
}
}
@@ -1,10 +1,15 @@
+use std::sync::Arc;
+
use chrono::DateTime;
use gpui2::geometry::{relative, rems, Size};
-use crate::prelude::*;
use crate::{
- theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide,
- ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
+ hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status,
+ Livestream,
+};
+use crate::{
+ theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides,
+ PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
};
#[derive(Element, Default)]
@@ -17,6 +22,8 @@ pub struct WorkspaceElement {
impl WorkspaceElement {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx).clone();
+
let temp_size = rems(36.).into();
let root_group = PaneGroup::new_groups(
@@ -29,8 +36,15 @@ impl WorkspaceElement {
width: relative(1.).into(),
height: temp_size,
},
- |_, _| vec![Terminal::new().into_any()],
- Box::new(()),
+ |_, payload| {
+ let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+ vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+ &theme,
+ ))
+ .into_any()]
+ },
+ Box::new(theme.clone()),
),
Pane::new(
ScrollState::default(),
@@ -51,8 +65,15 @@ impl WorkspaceElement {
width: relative(1.).into(),
height: relative(1.).into(),
},
- |_, _| vec![Terminal::new().into_any()],
- Box::new(()),
+ |_, payload| {
+ let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+ vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+ &theme,
+ ))
+ .into_any()]
+ },
+ Box::new(theme.clone()),
)],
SplitDirection::Vertical,
),
@@ -60,8 +81,6 @@ impl WorkspaceElement {
SplitDirection::Horizontal,
);
- let theme = theme(cx).clone();
-
div()
.size_full()
.flex()
@@ -72,7 +91,10 @@ impl WorkspaceElement {
.items_start()
.text_color(theme.lowest.base.default.foreground)
.fill(theme.lowest.base.default.background)
- .child(TitleBar::new(cx))
+ .child(TitleBar::new(cx).set_livestream(Some(Livestream {
+ players: random_players_with_call_status(7),
+ channel: Some("gpui2-ui".to_string()),
+ })))
.child(
div()
.flex_1()
@@ -84,8 +106,12 @@ impl WorkspaceElement {
.border_b()
.border_color(theme.lowest.base.default.border)
.child(
- ProjectPanel::new(self.left_panel_scroll_state.clone())
- .side(PanelSide::Left),
+ Panel::new(
+ self.left_panel_scroll_state.clone(),
+ |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+ Box::new(()),
+ )
+ .side(PanelSide::Left),
)
.child(
v_stack()
@@ -110,26 +136,37 @@ impl WorkspaceElement {
.side(PanelSide::Bottom),
),
)
- .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339(
- "2023-09-27T15:40:52.707Z",
- )
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339(
- "2023-09-28T15:40:52.707Z",
- )
- .unwrap()
- .naive_local(),
- ),
- ])),
+ .child(
+ Panel::new(
+ self.right_panel_scroll_state.clone(),
+ |_, payload| {
+ vec![ChatPanel::new(ScrollState::default())
+ .with_messages(vec![
+ ChatMessage::new(
+ "osiewicz".to_string(),
+ "is this thing on?".to_string(),
+ DateTime::parse_from_rfc3339(
+ "2023-09-27T15:40:52.707Z",
+ )
+ .unwrap()
+ .naive_local(),
+ ),
+ ChatMessage::new(
+ "maxdeviant".to_string(),
+ "Reading you loud and clear!".to_string(),
+ DateTime::parse_from_rfc3339(
+ "2023-09-28T15:40:52.707Z",
+ )
+ .unwrap()
+ .naive_local(),
+ ),
+ ])
+ .into_any()]
+ },
+ Box::new(()),
+ )
+ .side(PanelSide::Right),
+ ),
)
.child(StatusBar::new())
}
@@ -84,6 +84,7 @@ pub enum Icon {
Plus,
Quote,
Screen,
+ SelectAll,
Split,
SplitMessage,
Terminal,
@@ -131,6 +132,7 @@ impl Icon {
Icon::Plus => "icons/plus.svg",
Icon::Quote => "icons/quote.svg",
Icon::Screen => "icons/desktop.svg",
+ Icon::SelectAll => "icons/select-all.svg",
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
@@ -81,6 +81,7 @@ impl Input {
div()
.h_7()
+ .w_full()
.px_2()
.border()
.border_color(border_color_default)
@@ -65,7 +65,7 @@ impl PlayerCallStatus {
}
}
-#[derive(Clone)]
+#[derive(PartialEq, Clone)]
pub struct Player {
index: usize,
avatar_src: String,
@@ -73,6 +73,7 @@ pub struct Player {
status: PlayerStatus,
}
+#[derive(Clone)]
pub struct PlayerWithCallStatus {
player: Player,
call_status: PlayerCallStatus,
@@ -2,7 +2,7 @@ pub use gpui2::elements::div::{div, ScrollState};
pub use gpui2::style::{StyleHelpers, Styleable};
pub use gpui2::{Element, IntoElement, ParentElement, ViewContext};
-pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant};
+pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme};
use gpui2::{hsla, rgb, Hsla, WindowContext};
use strum::EnumIter;
@@ -40,8 +40,7 @@ pub enum HighlightColor {
}
impl HighlightColor {
- pub fn hsla(&self, cx: &WindowContext) -> Hsla {
- let theme = theme(cx);
+ pub fn hsla(&self, theme: &Theme) -> Hsla {
let system_color = SystemColor::new();
match self {
@@ -74,7 +73,7 @@ impl HighlightColor {
}
}
-#[derive(Default, PartialEq, EnumIter)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum FileSystemStatus {
#[default]
None,
@@ -92,7 +91,7 @@ impl FileSystemStatus {
}
}
-#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum GitStatus {
#[default]
None,
@@ -130,7 +129,7 @@ impl GitStatus {
}
}
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum DiagnosticStatus {
#[default]
None,
@@ -139,14 +138,14 @@ pub enum DiagnosticStatus {
Info,
}
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum IconSide {
#[default]
Left,
Right,
}
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum OrderMethod {
#[default]
Ascending,
@@ -154,14 +153,14 @@ pub enum OrderMethod {
MostRecent,
}
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum Shape {
#[default]
Circle,
RoundedRectangle,
}
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum DisclosureControlVisibility {
#[default]
OnHover,
@@ -1,12 +1,109 @@
-use gpui2::WindowContext;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use rand::Rng;
use crate::{
- Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText,
- Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus,
- ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus,
- ToggleState,
+ Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
+ HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry,
+ ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player,
+ PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState,
+ VideoStatus,
};
+pub fn static_tabs_example() -> Vec<Tab> {
+ vec![
+ Tab::new()
+ .title("wip.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false)
+ .fs_status(FileSystemStatus::Deleted),
+ Tab::new()
+ .title("Cargo.toml".to_string())
+ .icon(Icon::FileToml)
+ .current(false)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("Channels Panel".to_string())
+ .icon(Icon::Hash)
+ .current(false),
+ Tab::new()
+ .title("channels_panel.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(true)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("workspace.rs".to_string())
+ .current(false)
+ .icon(Icon::FileRust)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("icon_button.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false),
+ Tab::new()
+ .title("storybook.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false)
+ .git_status(GitStatus::Created),
+ Tab::new()
+ .title("theme.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false),
+ Tab::new()
+ .title("theme_registry.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false),
+ Tab::new()
+ .title("styleable_helpers.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false),
+ ]
+}
+
+pub fn static_tabs_1() -> Vec<Tab> {
+ vec![
+ Tab::new()
+ .title("project_panel.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false)
+ .fs_status(FileSystemStatus::Deleted),
+ Tab::new()
+ .title("tab_bar.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false)
+ .git_status(GitStatus::Modified),
+ Tab::new()
+ .title("workspace.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false),
+ Tab::new()
+ .title("tab.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(true)
+ .git_status(GitStatus::Modified),
+ ]
+}
+
+pub fn static_tabs_2() -> Vec<Tab> {
+ vec![
+ Tab::new()
+ .title("tab_bar.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(false)
+ .fs_status(FileSystemStatus::Deleted),
+ Tab::new()
+ .title("static_data.rs".to_string())
+ .icon(Icon::FileRust)
+ .current(true)
+ .git_status(GitStatus::Modified),
+ ]
+}
+
+pub fn static_tabs_3() -> Vec<Tab> {
+ vec![Tab::new().git_status(GitStatus::Created).current(true)]
+}
+
pub fn static_players() -> Vec<Player> {
vec![
Player::new(
@@ -37,6 +134,154 @@ pub fn static_players() -> Vec<Player> {
]
}
+#[derive(Debug)]
+pub struct PlayerData {
+ pub url: String,
+ pub name: String,
+}
+pub fn static_player_data() -> Vec<PlayerData> {
+ vec![
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+ name: "iamnbutler".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+ name: "maxbrunsfeld".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+ name: "as-cii".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
+ name: "nathansobo".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+ name: "ForLoveOfCats".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
+ name: "SomeoneToIgnore".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
+ name: "JosephTLyons".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
+ name: "osiewicz".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
+ name: "KCaverly".into(),
+ },
+ PlayerData {
+ url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+ name: "maxdeviant".into(),
+ },
+ ]
+}
+pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
+ let mut players = Vec::new();
+ for data in player_data {
+ players.push(Player::new(players.len(), data.url, data.name));
+ }
+ players
+}
+pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
+ Player::new(1, data[0].url.clone(), data[0].name.clone())
+}
+pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
+ Player::new(2, data[1].url.clone(), data[1].name.clone())
+}
+pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
+ Player::new(3, data[2].url.clone(), data[2].name.clone())
+}
+pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
+ Player::new(4, data[3].url.clone(), data[3].name.clone())
+}
+pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
+ Player::new(5, data[4].url.clone(), data[4].name.clone())
+}
+pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
+ Player::new(6, data[5].url.clone(), data[5].name.clone())
+}
+pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
+ Player::new(7, data[6].url.clone(), data[6].name.clone())
+}
+pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
+ Player::new(8, data[7].url.clone(), data[7].name.clone())
+}
+pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
+ Player::new(9, data[8].url.clone(), data[8].name.clone())
+}
+pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
+ Player::new(10, data[9].url.clone(), data[9].name.clone())
+}
+pub fn static_livestream() -> Livestream {
+ Livestream {
+ players: random_players_with_call_status(7),
+ channel: Some("gpui2-ui".to_string()),
+ }
+}
+pub fn populate_player_call_status(
+ player: Player,
+ followers: Option<Vec<Player>>,
+) -> PlayerCallStatus {
+ let mut rng = rand::thread_rng();
+ let in_current_project: bool = rng.gen();
+ let disconnected: bool = rng.gen();
+ let voice_activity: f32 = rng.gen();
+ let mic_status = if rng.gen_bool(0.5) {
+ MicStatus::Muted
+ } else {
+ MicStatus::Unmuted
+ };
+ let video_status = if rng.gen_bool(0.5) {
+ VideoStatus::On
+ } else {
+ VideoStatus::Off
+ };
+ let screen_share_status = if rng.gen_bool(0.5) {
+ ScreenShareStatus::Shared
+ } else {
+ ScreenShareStatus::NotShared
+ };
+ PlayerCallStatus {
+ mic_status,
+ voice_activity,
+ video_status,
+ screen_share_status,
+ in_current_project,
+ disconnected,
+ following: None,
+ followers,
+ }
+}
+pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
+ let players = create_static_players(static_player_data());
+ let mut player_status = vec![];
+ for i in 0..number_of_players {
+ let followers = if i == 0 {
+ Some(vec![
+ players[1].clone(),
+ players[3].clone(),
+ players[5].clone(),
+ players[6].clone(),
+ ])
+ } else if i == 1 {
+ Some(vec![players[2].clone(), players[6].clone()])
+ } else {
+ None
+ };
+ let call_status = populate_player_call_status(players[i].clone(), followers);
+ player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
+ }
+ player_status
+}
+
pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
let players = static_players();
let mut player_0_status = PlayerCallStatus::new();
@@ -123,7 +368,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
.left_icon(Icon::FolderOpen.into())
.indent_level(3)
.set_toggle(ToggleState::Toggled),
- ListEntry::new(Label::new("derrive_element.rs"))
+ ListEntry::new(Label::new("derive_element.rs"))
.left_icon(Icon::FileRust.into())
.indent_level(4),
ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
@@ -337,33 +582,49 @@ pub fn example_editor_actions() -> Vec<PaletteItem> {
]
}
-pub fn empty_buffer_example<V: 'static>() -> Buffer<V> {
+pub fn empty_editor_example() -> Editor {
+ Editor {
+ tabs: static_tabs_example(),
+ path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+ symbols: vec![],
+ buffer: empty_buffer_example(),
+ }
+}
+
+pub fn empty_buffer_example() -> Buffer {
Buffer::new().set_rows(Some(BufferRows::default()))
}
-pub fn hello_world_rust_buffer_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
- Buffer::new()
- .set_title("hello_world.rs".to_string())
- .set_path("src/hello_world.rs".to_string())
- .set_language("rust".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: true,
- rows: hello_world_rust_buffer_rows(cx),
- }))
+pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor {
+ Editor {
+ tabs: static_tabs_example(),
+ path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+ symbols: vec![Symbol(vec![
+ HighlightedText {
+ text: "fn ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "main".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ])],
+ buffer: hello_world_rust_buffer_example(theme),
+ }
}
-pub fn hello_world_rust_buffer_with_status_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
+pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer {
Buffer::new()
.set_title("hello_world.rs".to_string())
.set_path("src/hello_world.rs".to_string())
.set_language("rust".to_string())
.set_rows(Some(BufferRows {
show_line_numbers: true,
- rows: hello_world_rust_with_status_buffer_rows(cx),
+ rows: hello_world_rust_buffer_rows(theme),
}))
}
-pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
let show_line_number = true;
vec![
@@ -375,15 +636,15 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
highlighted_texts: vec![
HighlightedText {
text: "fn ".to_string(),
- color: HighlightColor::Keyword.hsla(cx),
+ color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "main".to_string(),
- color: HighlightColor::Function.hsla(cx),
+ color: HighlightColor::Function.hsla(&theme),
},
HighlightedText {
text: "() {".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ color: HighlightColor::Default.hsla(&theme),
},
],
}),
@@ -399,7 +660,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
highlighted_texts: vec![HighlightedText {
text: " // Statements here are executed when the compiled binary is called."
.to_string(),
- color: HighlightColor::Comment.hsla(cx),
+ color: HighlightColor::Comment.hsla(&theme),
}],
}),
cursors: None,
@@ -422,7 +683,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
text: " // Print text to the console.".to_string(),
- color: HighlightColor::Comment.hsla(cx),
+ color: HighlightColor::Comment.hsla(&theme),
}],
}),
cursors: None,
@@ -433,10 +694,34 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
line_number: 5,
code_action: false,
current: false,
+ line: Some(HighlightedLine {
+ highlighted_texts: vec![
+ HighlightedText {
+ text: " println!(".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ HighlightedText {
+ text: "\"Hello, world!\"".to_string(),
+ color: HighlightColor::String.hsla(&theme),
+ },
+ HighlightedText {
+ text: ");".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ ],
+ }),
+ cursors: None,
+ status: GitStatus::None,
+ show_line_number,
+ },
+ BufferRow {
+ line_number: 6,
+ code_action: false,
+ current: false,
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
text: "}".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ color: HighlightColor::Default.hsla(&theme),
}],
}),
cursors: None,
@@ -446,7 +731,36 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
]
}
-pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor {
+ Editor {
+ tabs: static_tabs_example(),
+ path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+ symbols: vec![Symbol(vec![
+ HighlightedText {
+ text: "fn ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "main".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ ])],
+ buffer: hello_world_rust_buffer_with_status_example(theme),
+ }
+}
+
+pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer {
+ Buffer::new()
+ .set_title("hello_world.rs".to_string())
+ .set_path("src/hello_world.rs".to_string())
+ .set_language("rust".to_string())
+ .set_rows(Some(BufferRows {
+ show_line_numbers: true,
+ rows: hello_world_rust_with_status_buffer_rows(theme),
+ }))
+}
+
+pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
let show_line_number = true;
vec![
@@ -458,15 +772,15 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
highlighted_texts: vec![
HighlightedText {
text: "fn ".to_string(),
- color: HighlightColor::Keyword.hsla(cx),
+ color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "main".to_string(),
- color: HighlightColor::Function.hsla(cx),
+ color: HighlightColor::Function.hsla(&theme),
},
HighlightedText {
text: "() {".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ color: HighlightColor::Default.hsla(&theme),
},
],
}),
@@ -482,7 +796,7 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
highlighted_texts: vec![HighlightedText {
text: "// Statements here are executed when the compiled binary is called."
.to_string(),
- color: HighlightColor::Comment.hsla(cx),
+ color: HighlightColor::Comment.hsla(&theme),
}],
}),
cursors: None,
@@ -505,7 +819,7 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
text: " // Print text to the console.".to_string(),
- color: HighlightColor::Comment.hsla(cx),
+ color: HighlightColor::Comment.hsla(&theme),
}],
}),
cursors: None,
@@ -516,10 +830,34 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
line_number: 5,
code_action: false,
current: false,
+ line: Some(HighlightedLine {
+ highlighted_texts: vec![
+ HighlightedText {
+ text: " println!(".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ HighlightedText {
+ text: "\"Hello, world!\"".to_string(),
+ color: HighlightColor::String.hsla(&theme),
+ },
+ HighlightedText {
+ text: ");".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ ],
+ }),
+ cursors: None,
+ status: GitStatus::None,
+ show_line_number,
+ },
+ BufferRow {
+ line_number: 6,
+ code_action: false,
+ current: false,
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
text: "}".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ color: HighlightColor::Default.hsla(&theme),
}],
}),
cursors: None,
@@ -527,13 +865,13 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
show_line_number,
},
BufferRow {
- line_number: 6,
+ line_number: 7,
code_action: false,
current: false,
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
text: "".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ color: HighlightColor::Default.hsla(&theme),
}],
}),
cursors: None,
@@ -541,13 +879,13 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
show_line_number,
},
BufferRow {
- line_number: 7,
+ line_number: 8,
code_action: false,
current: false,
line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText {
- text: "Marshall and Nate were here".to_string(),
- color: HighlightColor::Default.hsla(cx),
+ text: "// Marshall and Nate were here".to_string(),
+ color: HighlightColor::Comment.hsla(&theme),
}],
}),
cursors: None,
@@ -556,3 +894,73 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
},
]
}
+
+pub fn terminal_buffer(theme: &Theme) -> Buffer {
+ Buffer::new()
+ .set_title("zed — fish".to_string())
+ .set_rows(Some(BufferRows {
+ show_line_numbers: false,
+ rows: terminal_buffer_rows(theme),
+ }))
+}
+
+pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+ let show_line_number = false;
+
+ vec![
+ BufferRow {
+ line_number: 1,
+ code_action: false,
+ current: false,
+ line: Some(HighlightedLine {
+ highlighted_texts: vec![
+ HighlightedText {
+ text: "maxdeviant ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ HighlightedText {
+ text: "in ".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ HighlightedText {
+ text: "profaned-capital ".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ HighlightedText {
+ text: "in ".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ HighlightedText {
+ text: "~/p/zed ".to_string(),
+ color: HighlightColor::Function.hsla(&theme),
+ },
+ HighlightedText {
+ text: "on ".to_string(),
+ color: HighlightColor::Default.hsla(&theme),
+ },
+ HighlightedText {
+ text: " gpui2-ui ".to_string(),
+ color: HighlightColor::Keyword.hsla(&theme),
+ },
+ ],
+ }),
+ cursors: None,
+ status: GitStatus::None,
+ show_line_number,
+ },
+ BufferRow {
+ line_number: 2,
+ code_action: false,
+ current: false,
+ line: Some(HighlightedLine {
+ highlighted_texts: vec![HighlightedText {
+ text: "λ ".to_string(),
+ color: HighlightColor::String.hsla(&theme),
+ }],
+ }),
+ cursors: None,
+ status: GitStatus::None,
+ show_line_number,
+ },
+ ]
+}
@@ -171,7 +171,7 @@ impl Vim {
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
Event::SelectionsChanged { local: true } => {
let editor = editor.read(cx);
- if editor.leader_replica_id().is_none() {
+ if editor.leader_peer_id().is_none() {
let newest = editor.selections.newest::<usize>(cx);
local_selections_changed(newest, cx);
}
@@ -195,6 +195,8 @@ impl Vim {
if editor_mode == EditorMode::Full
&& !newest_selection_empty
&& self.state().mode == Mode::Normal
+ // When following someone, don't switch vim mode.
+ && editor.leader_peer_id().is_none()
{
self.switch_mode(Mode::Visual, true, cx);
}
@@ -4,7 +4,10 @@ use crate::{
};
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result;
-use client::{proto, Client};
+use client::{
+ proto::{self, PeerId},
+ Client,
+};
use gpui::geometry::vector::Vector2F;
use gpui::AnyWindowHandle;
use gpui::{
@@ -401,6 +404,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if let Some(followed_item) = self.to_followable_item_handle(cx) {
if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers(
+ followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item
.remote_id(&workspace.app_state.client, cx)
@@ -436,6 +440,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
};
if let Some(item) = item.to_followable_item_handle(cx) {
+ let is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane);
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
@@ -455,6 +460,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
move |this, cx| {
pending_update_scheduled.store(false, Ordering::SeqCst);
this.update_followers(
+ is_project_item,
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item
@@ -692,14 +698,15 @@ pub trait FollowableItem: Item {
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>>;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
- fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+ fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
}
pub trait FollowableItemHandle: ItemHandle {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
- fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext);
+ fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto(
&self,
@@ -714,6 +721,7 @@ pub trait FollowableItemHandle: ItemHandle {
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
}
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
@@ -726,10 +734,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
})
}
- fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext) {
- self.update(cx, |this, cx| {
- this.set_leader_replica_id(leader_replica_id, cx)
- })
+ fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
@@ -765,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
false
}
}
+
+ fn is_project_item(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_project_item(cx)
+ }
}
#[cfg(any(test, feature = "test-support"))]
@@ -190,25 +190,23 @@ impl Member {
})
.and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx);
- let collaborator = project.read(cx).collaborators().get(leader_id)?;
- let participant = room.remote_participant_for_peer_id(*leader_id)?;
- Some((collaborator.replica_id, participant))
+ room.remote_participant_for_peer_id(*leader_id)
});
- let border = if let Some((replica_id, _)) = leader.as_ref() {
- let leader_color = theme.editor.replica_selection_style(*replica_id).cursor;
- let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
- border
+ let mut leader_border = Border::default();
+ let mut leader_status_box = None;
+ if let Some(leader) = &leader {
+ let leader_color = theme
+ .editor
+ .selection_style_for_room_participant(leader.participant_index.0)
+ .cursor;
+ leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
+ leader_border
.color
.fade_out(1. - theme.workspace.leader_border_opacity);
- border.overlay = true;
- border
- } else {
- Border::default()
- };
+ leader_border.overlay = true;
- let leader_status_box = if let Some((_, leader)) = leader {
- match leader.location {
+ leader_status_box = match leader.location {
ParticipantLocation::SharedProject {
project_id: leader_project_id,
} => {
@@ -217,7 +215,6 @@ impl Member {
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
- let app_state = Arc::downgrade(app_state);
Some(
MouseEventHandler::new::<FollowIntoExternalProject, _>(
pane.id(),
@@ -225,7 +222,7 @@ impl Member {
|_, _| {
Label::new(
format!(
- "Follow {} on their active project",
+ "Follow {} to their active project",
leader_user.github_login,
),
theme
@@ -241,16 +238,14 @@ impl Member {
},
)
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- if let Some(app_state) = app_state.upgrade() {
- crate::join_remote_project(
- leader_project_id,
- leader_user_id,
- app_state,
- cx,
- )
- .detach_and_log_err(cx);
- }
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ crate::join_remote_project(
+ leader_project_id,
+ leader_user_id,
+ this.app_state().clone(),
+ cx,
+ )
+ .detach_and_log_err(cx);
})
.aligned()
.bottom()
@@ -289,13 +284,11 @@ impl Member {
.right()
.into_any(),
),
- }
- } else {
- None
- };
+ };
+ }
Stack::new()
- .with_child(pane_element.contained().with_border(border))
+ .with_child(pane_element.contained().with_border(leader_border))
.with_children(leader_status_box)
.into_any()
}
@@ -375,11 +375,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
})
.detach();
});
-
- let client = &app_state.client;
- client.add_view_request_handler(Workspace::handle_follow);
- client.add_view_message_handler(Workspace::handle_unfollow);
- client.add_view_message_handler(Workspace::handle_update_followers);
}
type ProjectItemBuilders = HashMap<
@@ -456,6 +451,7 @@ pub struct AppState {
pub client: Arc<Client>,
pub user_store: ModelHandle<UserStore>,
pub channel_store: ModelHandle<ChannelStore>,
+ pub workspace_store: ModelHandle<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@@ -464,6 +460,19 @@ pub struct AppState {
pub background_actions: BackgroundActions,
}
+pub struct WorkspaceStore {
+ workspaces: HashSet<WeakViewHandle<Workspace>>,
+ followers: Vec<Follower>,
+ client: Arc<Client>,
+ _subscriptions: Vec<client::Subscription>,
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
+struct Follower {
+ project_id: Option<u64>,
+ peer_id: PeerId,
+}
+
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -480,6 +489,7 @@ impl AppState {
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init((), cx);
client::init(&client, cx);
@@ -491,6 +501,7 @@ impl AppState {
languages,
user_store,
channel_store,
+ workspace_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
background_actions: || &[],
@@ -551,7 +562,6 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakViewHandle<Self>,
- remote_entity_subscription: Option<client::Subscription>,
modal: Option<ActiveModal>,
zoomed: Option<AnyWeakViewHandle>,
zoomed_position: Option<DockPosition>,
@@ -567,7 +577,6 @@ pub struct Workspace {
titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
- leader_state: LeaderState,
follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
@@ -593,11 +602,6 @@ pub struct ViewId {
pub id: u64,
}
-#[derive(Default)]
-struct LeaderState {
- followers: HashSet<PeerId>,
-}
-
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
@@ -618,9 +622,8 @@ impl Workspace {
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(&project, move |this, _, event, cx| {
match event {
- project::Event::RemoteIdChanged(remote_id) => {
+ project::Event::RemoteIdChanged(_) => {
this.update_window_title(cx);
- this.project_remote_id_changed(*remote_id, cx);
}
project::Event::CollaboratorLeft(peer_id) => {
@@ -675,6 +678,10 @@ impl Workspace {
cx.focus(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
+ app_state.workspace_store.update(cx, |store, _| {
+ store.workspaces.insert(weak_handle.clone());
+ });
+
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn(|this, mut cx| async move {
@@ -768,7 +775,8 @@ impl Workspace {
}),
];
- let mut this = Workspace {
+ cx.defer(|this, cx| this.update_window_title(cx));
+ Workspace {
weak_self: weak_handle.clone(),
modal: None,
zoomed: None,
@@ -781,12 +789,10 @@ impl Workspace {
status_bar,
titlebar_item: None,
notifications: Default::default(),
- remote_entity_subscription: None,
left_dock,
bottom_dock,
right_dock,
project: project.clone(),
- leader_state: Default::default(),
follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
@@ -799,10 +805,7 @@ impl Workspace {
leader_updates_tx,
subscriptions,
pane_history_timestamp,
- };
- this.project_remote_id_changed(project.read(cx).remote_id(), cx);
- cx.defer(|this, cx| this.update_window_title(cx));
- this
+ }
}
fn new_local(
@@ -2506,43 +2509,24 @@ impl Workspace {
&self.active_pane
}
- fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
- if let Some(remote_id) = remote_id {
- self.remote_entity_subscription = Some(
- self.app_state
- .client
- .add_view_for_remote_entity(remote_id, cx),
- );
- } else {
- self.remote_entity_subscription.take();
- }
- }
-
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- self.leader_state.followers.remove(&peer_id);
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
for state in states_by_pane.into_values() {
for item in state.items_by_leader_view_id.into_values() {
- item.set_leader_replica_id(None, cx);
+ item.set_leader_peer_id(None, cx);
}
}
}
cx.notify();
}
- pub fn toggle_follow(
+ fn start_following(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane = self.active_pane().clone();
- if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
- if leader_id == prev_leader_id {
- return None;
- }
- }
-
self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id);
self.follower_states_by_leader
@@ -2551,8 +2535,10 @@ impl Workspace {
.insert(pane.clone(), Default::default());
cx.notify();
- let project_id = self.project.read(cx).remote_id()?;
+ let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+ let project_id = self.project.read(cx).remote_id();
let request = self.app_state.client.request(proto::Follow {
+ room_id,
project_id,
leader_id: Some(leader_id),
});
@@ -2611,9 +2597,64 @@ impl Workspace {
None
};
- next_leader_id
- .or_else(|| collaborators.keys().copied().next())
- .and_then(|leader_id| self.toggle_follow(leader_id, cx))
+ let pane = self.active_pane.clone();
+ let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
+ else {
+ return None;
+ };
+ if Some(leader_id) == self.unfollow(&pane, cx) {
+ return None;
+ }
+ self.follow(leader_id, cx)
+ }
+
+ pub fn follow(
+ &mut self,
+ leader_id: PeerId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+ let project = self.project.read(cx);
+
+ let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+ return None;
+ };
+
+ let other_project_id = match remote_participant.location {
+ call::ParticipantLocation::External => None,
+ call::ParticipantLocation::UnsharedProject => None,
+ call::ParticipantLocation::SharedProject { project_id } => {
+ if Some(project_id) == project.remote_id() {
+ None
+ } else {
+ Some(project_id)
+ }
+ }
+ };
+
+ // if they are active in another project, follow there.
+ if let Some(project_id) = other_project_id {
+ let app_state = self.app_state.clone();
+ return Some(crate::join_remote_project(
+ project_id,
+ remote_participant.user.id,
+ app_state,
+ cx,
+ ));
+ }
+
+ // if you're already following, find the right pane and focus it.
+ for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
+ if leader_id == *existing_leader_id {
+ for (pane, _) in states_by_pane {
+ cx.focus(pane);
+ return None;
+ }
+ }
+ }
+
+ // Otherwise, follow.
+ self.start_following(leader_id, cx)
}
pub fn unfollow(
@@ -2625,20 +2666,21 @@ impl Workspace {
let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id {
- item.set_leader_replica_id(None, cx);
+ item.set_leader_peer_id(None, cx);
}
if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id);
- if let Some(project_id) = self.project.read(cx).remote_id() {
- self.app_state
- .client
- .send(proto::Unfollow {
- project_id,
- leader_id: Some(leader_id),
- })
- .log_err();
- }
+ let project_id = self.project.read(cx).remote_id();
+ let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+ self.app_state
+ .client
+ .send(proto::Unfollow {
+ room_id,
+ project_id,
+ leader_id: Some(leader_id),
+ })
+ .log_err();
}
cx.notify();
@@ -2652,10 +2694,6 @@ impl Workspace {
self.follower_states_by_leader.contains_key(&peer_id)
}
- pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
- self.leader_state.followers.contains(&peer_id)
- }
-
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// TODO: There should be a better system in place for this
// (https://github.com/zed-industries/zed/issues/1290)
@@ -2806,81 +2844,64 @@ impl Workspace {
// RPC handlers
- async fn handle_follow(
- this: WeakViewHandle<Self>,
- envelope: TypedEnvelope<proto::Follow>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<proto::FollowResponse> {
- this.update(&mut cx, |this, cx| {
- let client = &this.app_state.client;
- this.leader_state
- .followers
- .insert(envelope.original_sender_id()?);
+ fn handle_follow(
+ &mut self,
+ follower_project_id: Option<u64>,
+ cx: &mut ViewContext<Self>,
+ ) -> proto::FollowResponse {
+ let client = &self.app_state.client;
+ let project_id = self.project.read(cx).remote_id();
- let active_view_id = this.active_item(cx).and_then(|i| {
- Some(
- i.to_followable_item_handle(cx)?
- .remote_id(client, cx)?
- .to_proto(),
- )
- });
+ let active_view_id = self.active_item(cx).and_then(|i| {
+ Some(
+ i.to_followable_item_handle(cx)?
+ .remote_id(client, cx)?
+ .to_proto(),
+ )
+ });
- cx.notify();
+ cx.notify();
- Ok(proto::FollowResponse {
- active_view_id,
- views: this
- .panes()
- .iter()
- .flat_map(|pane| {
- let leader_id = this.leader_for_pane(pane);
- pane.read(cx).items().filter_map({
- let cx = &cx;
- move |item| {
- let item = item.to_followable_item_handle(cx)?;
- let id = item.remote_id(client, cx)?.to_proto();
- let variant = item.to_state_proto(cx)?;
- Some(proto::View {
- id: Some(id),
- leader_id,
- variant: Some(variant),
- })
+ proto::FollowResponse {
+ active_view_id,
+ views: self
+ .panes()
+ .iter()
+ .flat_map(|pane| {
+ let leader_id = self.leader_for_pane(pane);
+ pane.read(cx).items().filter_map({
+ let cx = &cx;
+ move |item| {
+ let item = item.to_followable_item_handle(cx)?;
+ if project_id.is_some()
+ && project_id != follower_project_id
+ && item.is_project_item(cx)
+ {
+ return None;
}
- })
+ let id = item.remote_id(client, cx)?.to_proto();
+ let variant = item.to_state_proto(cx)?;
+ Some(proto::View {
+ id: Some(id),
+ leader_id,
+ variant: Some(variant),
+ })
+ }
})
- .collect(),
- })
- })?
- }
-
- async fn handle_unfollow(
- this: WeakViewHandle<Self>,
- envelope: TypedEnvelope<proto::Unfollow>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- this.leader_state
- .followers
- .remove(&envelope.original_sender_id()?);
- cx.notify();
- Ok(())
- })?
+ })
+ .collect(),
+ }
}
- async fn handle_update_followers(
- this: WeakViewHandle<Self>,
- envelope: TypedEnvelope<proto::UpdateFollowers>,
- _: Arc<Client>,
- cx: AsyncAppContext,
- ) -> Result<()> {
- let leader_id = envelope.original_sender_id()?;
- this.read_with(&cx, |this, _| {
- this.leader_updates_tx
- .unbounded_send((leader_id, envelope.payload))
- })??;
- Ok(())
+ fn handle_update_followers(
+ &mut self,
+ leader_id: PeerId,
+ message: proto::UpdateFollowers,
+ _cx: &mut ViewContext<Self>,
+ ) {
+ self.leader_updates_tx
+ .unbounded_send((leader_id, message))
+ .ok();
}
async fn process_leader_update(
@@ -2953,18 +2974,6 @@ impl Workspace {
let this = this
.upgrade(cx)
.ok_or_else(|| anyhow!("workspace dropped"))?;
- let project = this
- .read_with(cx, |this, _| this.project.clone())
- .ok_or_else(|| anyhow!("window dropped"))?;
-
- let replica_id = project
- .read_with(cx, |project, _| {
- project
- .collaborators()
- .get(&leader_id)
- .map(|c| c.replica_id)
- })
- .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
let item_builders = cx.update(|cx| {
cx.default_global::<FollowableItemBuilders>()
@@ -3009,7 +3018,7 @@ impl Workspace {
.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
- item.set_leader_replica_id(Some(replica_id), cx);
+ item.set_leader_peer_id(Some(leader_id), cx);
state.items_by_leader_view_id.insert(id, item);
}
@@ -3020,46 +3029,44 @@ impl Workspace {
}
fn update_active_view_for_followers(&self, cx: &AppContext) {
+ let mut is_project_item = true;
+ let mut update = proto::UpdateActiveView::default();
if self.active_pane.read(cx).has_focus() {
- self.update_followers(
- proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
- id: self.active_item(cx).and_then(|item| {
- item.to_followable_item_handle(cx)?
- .remote_id(&self.app_state.client, cx)
- .map(|id| id.to_proto())
- }),
+ let item = self
+ .active_item(cx)
+ .and_then(|item| item.to_followable_item_handle(cx));
+ if let Some(item) = item {
+ is_project_item = item.is_project_item(cx);
+ update = proto::UpdateActiveView {
+ id: item
+ .remote_id(&self.app_state.client, cx)
+ .map(|id| id.to_proto()),
leader_id: self.leader_for_pane(&self.active_pane),
- }),
- cx,
- );
- } else {
- self.update_followers(
- proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
- id: None,
- leader_id: None,
- }),
- cx,
- );
+ };
+ }
}
+
+ self.update_followers(
+ is_project_item,
+ proto::update_followers::Variant::UpdateActiveView(update),
+ cx,
+ );
}
fn update_followers(
&self,
+ project_only: bool,
update: proto::update_followers::Variant,
cx: &AppContext,
) -> Option<()> {
- let project_id = self.project.read(cx).remote_id()?;
- if !self.leader_state.followers.is_empty() {
- self.app_state
- .client
- .send(proto::UpdateFollowers {
- project_id,
- follower_ids: self.leader_state.followers.iter().copied().collect(),
- variant: Some(update),
- })
- .log_err();
- }
- None
+ let project_id = if project_only {
+ self.project.read(cx).remote_id()
+ } else {
+ None
+ };
+ self.app_state().workspace_store.read_with(cx, |store, cx| {
+ store.update_followers(project_id, update, cx)
+ })
}
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
@@ -3081,31 +3088,39 @@ impl Workspace {
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_activate = Vec::new();
+
+ let leader_in_this_app;
+ let leader_in_this_project;
match participant.location {
call::ParticipantLocation::SharedProject { project_id } => {
- if Some(project_id) == self.project.read(cx).remote_id() {
- for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
- if let Some(item) = state
- .active_view_id
- .and_then(|id| state.items_by_leader_view_id.get(&id))
- {
- items_to_activate.push((pane.clone(), item.boxed_clone()));
- } else if let Some(shared_screen) =
- self.shared_screen_for_peer(leader_id, pane, cx)
- {
- items_to_activate.push((pane.clone(), Box::new(shared_screen)));
- }
- }
- }
+ leader_in_this_app = true;
+ leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+ }
+ call::ParticipantLocation::UnsharedProject => {
+ leader_in_this_app = true;
+ leader_in_this_project = false;
}
- call::ParticipantLocation::UnsharedProject => {}
call::ParticipantLocation::External => {
- for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
- if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
- items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+ leader_in_this_app = false;
+ leader_in_this_project = false;
+ }
+ };
+
+ for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+ if leader_in_this_app {
+ let item = state
+ .active_view_id
+ .and_then(|id| state.items_by_leader_view_id.get(&id));
+ if let Some(item) = item {
+ if leader_in_this_project || !item.is_project_item(cx) {
+ items_to_activate.push((pane.clone(), item.boxed_clone()));
}
+ continue;
}
}
+ if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+ items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+ }
}
for (pane, item) in items_to_activate {
@@ -3149,6 +3164,7 @@ impl Workspace {
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active {
+ self.update_active_view_for_followers(cx);
cx.background()
.spawn(persistence::DB.update_timestamp(self.database_id()))
.detach();
@@ -3522,8 +3538,10 @@ impl Workspace {
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
+ workspace_store,
client,
user_store,
channel_store,
@@ -3767,6 +3785,12 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
impl Entity for Workspace {
type Event = Event;
+
+ fn release(&mut self, cx: &mut AppContext) {
+ self.app_state.workspace_store.update(cx, |store, _| {
+ store.workspaces.remove(&self.weak_self);
+ })
+ }
}
impl View for Workspace {
@@ -3909,6 +3933,151 @@ impl View for Workspace {
}
}
+impl WorkspaceStore {
+ pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
+ Self {
+ workspaces: Default::default(),
+ followers: Default::default(),
+ _subscriptions: vec![
+ client.add_request_handler(cx.handle(), Self::handle_follow),
+ client.add_message_handler(cx.handle(), Self::handle_unfollow),
+ client.add_message_handler(cx.handle(), Self::handle_update_followers),
+ ],
+ client,
+ }
+ }
+
+ pub fn update_followers(
+ &self,
+ project_id: Option<u64>,
+ update: proto::update_followers::Variant,
+ cx: &AppContext,
+ ) -> Option<()> {
+ if !cx.has_global::<ModelHandle<ActiveCall>>() {
+ return None;
+ }
+
+ let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
+ let follower_ids: Vec<_> = self
+ .followers
+ .iter()
+ .filter_map(|follower| {
+ if follower.project_id == project_id || project_id.is_none() {
+ Some(follower.peer_id.into())
+ } else {
+ None
+ }
+ })
+ .collect();
+ if follower_ids.is_empty() {
+ return None;
+ }
+ self.client
+ .send(proto::UpdateFollowers {
+ room_id,
+ project_id,
+ follower_ids,
+ variant: Some(update),
+ })
+ .log_err()
+ }
+
+ async fn handle_follow(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::Follow>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::FollowResponse> {
+ this.update(&mut cx, |this, cx| {
+ let follower = Follower {
+ project_id: envelope.payload.project_id,
+ peer_id: envelope.original_sender_id()?,
+ };
+ let active_project = ActiveCall::global(cx)
+ .read(cx)
+ .location()
+ .map(|project| project.id());
+
+ let mut response = proto::FollowResponse::default();
+ for workspace in &this.workspaces {
+ let Some(workspace) = workspace.upgrade(cx) else {
+ continue;
+ };
+
+ workspace.update(cx.as_mut(), |workspace, cx| {
+ let handler_response = workspace.handle_follow(follower.project_id, cx);
+ if response.views.is_empty() {
+ response.views = handler_response.views;
+ } else {
+ response.views.extend_from_slice(&handler_response.views);
+ }
+
+ if let Some(active_view_id) = handler_response.active_view_id.clone() {
+ if response.active_view_id.is_none()
+ || Some(workspace.project.id()) == active_project
+ {
+ response.active_view_id = Some(active_view_id);
+ }
+ }
+ });
+ }
+
+ if let Err(ix) = this.followers.binary_search(&follower) {
+ this.followers.insert(ix, follower);
+ }
+
+ Ok(response)
+ })
+ }
+
+ async fn handle_unfollow(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::Unfollow>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ this.update(&mut cx, |this, _| {
+ let follower = Follower {
+ project_id: envelope.payload.project_id,
+ peer_id: envelope.original_sender_id()?,
+ };
+ if let Ok(ix) = this.followers.binary_search(&follower) {
+ this.followers.remove(ix);
+ }
+ Ok(())
+ })
+ }
+
+ async fn handle_update_followers(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::UpdateFollowers>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ let leader_id = envelope.original_sender_id()?;
+ let update = envelope.payload;
+ this.update(&mut cx, |this, cx| {
+ for workspace in &this.workspaces {
+ let Some(workspace) = workspace.upgrade(cx) else {
+ continue;
+ };
+ workspace.update(cx.as_mut(), |workspace, cx| {
+ let project_id = workspace.project.read(cx).remote_id();
+ if update.project_id != project_id && update.project_id.is_some() {
+ return;
+ }
+ workspace.handle_update_followers(leader_id, update.clone(), cx);
+ });
+ }
+ Ok(())
+ })
+ }
+}
+
+impl Entity for WorkspaceStore {
+ type Event = ();
+}
+
impl ViewId {
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
Ok(Self {
@@ -4077,21 +4246,20 @@ pub fn join_remote_project(
cx: &mut AppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
- let existing_workspace = cx
- .windows()
- .into_iter()
- .find_map(|window| {
- window.downcast::<Workspace>().and_then(|window| {
- window.read_root_with(&cx, |workspace, cx| {
+ let windows = cx.windows();
+ let existing_workspace = windows.into_iter().find_map(|window| {
+ window.downcast::<Workspace>().and_then(|window| {
+ window
+ .read_root_with(&cx, |workspace, cx| {
if workspace.project().read(cx).remote_id() == Some(project_id) {
Some(cx.handle().downgrade())
} else {
None
}
})
- })
+ .unwrap_or(None)
})
- .flatten();
+ });
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
@@ -4156,11 +4324,9 @@ pub fn join_remote_project(
});
if let Some(follow_peer_id) = follow_peer_id {
- if !workspace.is_being_followed(follow_peer_id) {
- workspace
- .toggle_follow(follow_peer_id, cx)
- .map(|follow| follow.detach_and_log_err(cx));
- }
+ workspace
+ .follow(follow_peer_id, cx)
+ .map(|follow| follow.detach_and_log_err(cx));
}
}
})?;
@@ -6,12 +6,11 @@ use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc};
use util::asset_str;
-use self::elixir_next::ElixirSettings;
+use self::elixir::ElixirSettings;
mod c;
mod css;
mod elixir;
-mod elixir_next;
mod go;
mod html;
mod json;
@@ -46,7 +45,7 @@ pub fn init(
node_runtime: Arc<dyn NodeRuntime>,
cx: &mut AppContext,
) {
- settings::register::<elixir_next::ElixirSettings>(cx);
+ settings::register::<elixir::ElixirSettings>(cx);
let language = |name, grammar, adapters| {
languages.register(name, load_config(name), grammar, adapters, load_queries)
@@ -72,21 +71,21 @@ pub fn init(
],
);
- match &settings::get::<ElixirSettings>(cx).next {
- elixir_next::ElixirNextSetting::Off => language(
+ match &settings::get::<ElixirSettings>(cx).lsp {
+ elixir::ElixirLspSetting::ElixirLs => language(
"elixir",
tree_sitter_elixir::language(),
vec![Arc::new(elixir::ElixirLspAdapter)],
),
- elixir_next::ElixirNextSetting::On => language(
+ elixir::ElixirLspSetting::NextLs => language(
"elixir",
tree_sitter_elixir::language(),
- vec![Arc::new(elixir_next::NextLspAdapter)],
+ vec![Arc::new(elixir::NextLspAdapter)],
),
- elixir_next::ElixirNextSetting::Local { path, arguments } => language(
+ elixir::ElixirLspSetting::Local { path, arguments } => language(
"elixir",
tree_sitter_elixir::language(),
- vec![Arc::new(elixir_next::LocalNextLspAdapter {
+ vec![Arc::new(elixir::LocalLspAdapter {
path: path.clone(),
arguments: arguments.clone(),
})],
@@ -1,12 +1,17 @@
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
pub use language::*;
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
use smol::fs::{self, File};
use std::{
any::Any,
+ env::consts,
+ ops::Deref,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
@@ -14,11 +19,50 @@ use std::{
},
};
use util::{
+ async_iife,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
};
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+ pub lsp: ElixirLspSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirLspSetting {
+ ElixirLs,
+ NextLs,
+ Local {
+ path: String,
+ arguments: Vec<String>,
+ },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+ lsp: Option<ElixirLspSetting>,
+}
+
+impl Setting for ElixirSettings {
+ const KEY: Option<&'static str> = Some("elixir");
+
+ type FileContent = ElixirSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> Result<Self>
+ where
+ Self: Sized,
+ {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
+
pub struct ElixirLspAdapter;
#[async_trait]
@@ -144,14 +188,14 @@ impl LspAdapter for ElixirLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- get_cached_server_binary(container_dir).await
+ get_cached_server_binary_elixir_ls(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
- get_cached_server_binary(container_dir).await
+ get_cached_server_binary_elixir_ls(container_dir).await
}
async fn label_for_completion(
@@ -238,7 +282,9 @@ impl LspAdapter for ElixirLspAdapter {
}
}
-async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+async fn get_cached_server_binary_elixir_ls(
+ container_dir: PathBuf,
+) -> Option<LanguageServerBinary> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
@@ -254,3 +300,247 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
.await
.log_err()
}
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("next-ls".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "next-ls"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Send + Any>> {
+ let release =
+ latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+ let version = release.name.clone();
+ let platform = match consts::ARCH {
+ "x86_64" => "darwin_arm64",
+ "aarch64" => "darwin_amd64",
+ other => bail!("Running on unsupported platform: {other}"),
+ };
+ let asset_name = format!("next_ls_{}", platform);
+ let asset = release
+ .assets
+ .iter()
+ .find(|asset| asset.name == asset_name)
+ .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+ let version = GitHubLspBinaryVersion {
+ name: version,
+ url: asset.browser_download_url.clone(),
+ };
+ Ok(Box::new(version) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ version: Box<dyn 'static + Send + Any>,
+ container_dir: PathBuf,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+ let binary_path = container_dir.join("next-ls");
+
+ if fs::metadata(&binary_path).await.is_err() {
+ let mut response = delegate
+ .http_client()
+ .get(&version.url, Default::default(), true)
+ .await
+ .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+ let mut file = smol::fs::File::create(&binary_path).await?;
+ if !response.status().is_success() {
+ Err(anyhow!(
+ "download failed with status {}",
+ response.status().to_string()
+ ))?;
+ }
+ futures::io::copy(response.body_mut(), &mut file).await?;
+
+ fs::set_permissions(
+ &binary_path,
+ <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+ )
+ .await?;
+ }
+
+ Ok(LanguageServerBinary {
+ path: binary_path,
+ arguments: vec!["--stdio".into()],
+ })
+ }
+
+ async fn cached_server_binary(
+ &self,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary_next(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--stdio".into()];
+ binary
+ })
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary_next(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
+ })
+ }
+
+ async fn label_for_completion(
+ &self,
+ completion: &lsp::CompletionItem,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_completion_elixir(completion, language)
+ }
+
+ async fn label_for_symbol(
+ &self,
+ name: &str,
+ symbol_kind: SymbolKind,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_symbol_elixir(name, symbol_kind, language)
+ }
+}
+
+async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ async_iife!({
+ let mut last_binary_path = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_file()
+ && entry
+ .file_name()
+ .to_str()
+ .map_or(false, |name| name == "next-ls")
+ {
+ last_binary_path = Some(entry.path());
+ }
+ }
+
+ if let Some(path) = last_binary_path {
+ Ok(LanguageServerBinary {
+ path,
+ arguments: Vec::new(),
+ })
+ } else {
+ Err(anyhow!("no cached binary"))
+ }
+ })
+ .await
+ .log_err()
+}
+
+pub struct LocalLspAdapter {
+ pub path: String,
+ pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalLspAdapter {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("local-ls".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "local-ls"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Send + Any>> {
+ Ok(Box::new(()) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _: Box<dyn 'static + Send + Any>,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let path = shellexpand::full(&self.path)?;
+ Ok(LanguageServerBinary {
+ path: PathBuf::from(path.deref()),
+ arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+ })
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ let path = shellexpand::full(&self.path).ok()?;
+ Some(LanguageServerBinary {
+ path: PathBuf::from(path.deref()),
+ arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+ })
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ let path = shellexpand::full(&self.path).ok()?;
+ Some(LanguageServerBinary {
+ path: PathBuf::from(path.deref()),
+ arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+ })
+ }
+
+ async fn label_for_completion(
+ &self,
+ completion: &lsp::CompletionItem,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_completion_elixir(completion, language)
+ }
+
+ async fn label_for_symbol(
+ &self,
+ name: &str,
+ symbol: SymbolKind,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_symbol_elixir(name, symbol, language)
+ }
+}
+
+fn label_for_completion_elixir(
+ completion: &lsp::CompletionItem,
+ language: &Arc<Language>,
+) -> Option<CodeLabel> {
+ return Some(CodeLabel {
+ runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+ text: completion.label.clone(),
+ filter_range: 0..completion.label.len(),
+ });
+}
+
+fn label_for_symbol_elixir(
+ name: &str,
+ _: SymbolKind,
+ language: &Arc<Language>,
+) -> Option<CodeLabel> {
+ Some(CodeLabel {
+ runs: language.highlight_text(&name.into(), 0..name.len()),
+ text: name.to_string(),
+ filter_range: 0..name.len(),
+ })
+}
@@ -1,266 +0,0 @@
-use anyhow::{anyhow, bail, Result};
-
-use async_trait::async_trait;
-pub use language::*;
-use lsp::{LanguageServerBinary, SymbolKind};
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
-use smol::{fs, stream::StreamExt};
-use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc};
-use util::{
- async_iife,
- github::{latest_github_release, GitHubLspBinaryVersion},
- ResultExt,
-};
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
-pub struct ElixirSettings {
- pub next: ElixirNextSetting,
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ElixirNextSetting {
- Off,
- On,
- Local {
- path: String,
- arguments: Vec<String>,
- },
-}
-
-#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
-pub struct ElixirSettingsContent {
- next: Option<ElixirNextSetting>,
-}
-
-impl Setting for ElixirSettings {
- const KEY: Option<&'static str> = Some("elixir");
-
- type FileContent = ElixirSettingsContent;
-
- fn load(
- default_value: &Self::FileContent,
- user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
- ) -> Result<Self>
- where
- Self: Sized,
- {
- Self::load_via_json_merge(default_value, user_values)
- }
-}
-
-pub struct NextLspAdapter;
-
-#[async_trait]
-impl LspAdapter for NextLspAdapter {
- async fn name(&self) -> LanguageServerName {
- LanguageServerName("next-ls".into())
- }
-
- fn short_name(&self) -> &'static str {
- "next-ls"
- }
-
- async fn fetch_latest_server_version(
- &self,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<Box<dyn 'static + Send + Any>> {
- let release =
- latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
- let version = release.name.clone();
- let platform = match consts::ARCH {
- "x86_64" => "darwin_arm64",
- "aarch64" => "darwin_amd64",
- other => bail!("Running on unsupported platform: {other}"),
- };
- let asset_name = format!("next_ls_{}", platform);
- let asset = release
- .assets
- .iter()
- .find(|asset| asset.name == asset_name)
- .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
- let version = GitHubLspBinaryVersion {
- name: version,
- url: asset.browser_download_url.clone(),
- };
- Ok(Box::new(version) as Box<_>)
- }
-
- async fn fetch_server_binary(
- &self,
- version: Box<dyn 'static + Send + Any>,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
-
- let binary_path = container_dir.join("next-ls");
-
- if fs::metadata(&binary_path).await.is_err() {
- let mut response = delegate
- .http_client()
- .get(&version.url, Default::default(), true)
- .await
- .map_err(|err| anyhow!("error downloading release: {}", err))?;
-
- let mut file = smol::fs::File::create(&binary_path).await?;
- if !response.status().is_success() {
- Err(anyhow!(
- "download failed with status {}",
- response.status().to_string()
- ))?;
- }
- futures::io::copy(response.body_mut(), &mut file).await?;
-
- fs::set_permissions(
- &binary_path,
- <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
- )
- .await?;
- }
-
- Ok(LanguageServerBinary {
- path: binary_path,
- arguments: vec!["--stdio".into()],
- })
- }
-
- async fn cached_server_binary(
- &self,
- container_dir: PathBuf,
- _: &dyn LspAdapterDelegate,
- ) -> Option<LanguageServerBinary> {
- get_cached_server_binary(container_dir)
- .await
- .map(|mut binary| {
- binary.arguments = vec!["--stdio".into()];
- binary
- })
- }
-
- async fn installation_test_binary(
- &self,
- container_dir: PathBuf,
- ) -> Option<LanguageServerBinary> {
- get_cached_server_binary(container_dir)
- .await
- .map(|mut binary| {
- binary.arguments = vec!["--help".into()];
- binary
- })
- }
-
- async fn label_for_symbol(
- &self,
- name: &str,
- symbol_kind: SymbolKind,
- language: &Arc<Language>,
- ) -> Option<CodeLabel> {
- label_for_symbol_next(name, symbol_kind, language)
- }
-}
-
-async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
- async_iife!({
- let mut last_binary_path = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_file()
- && entry
- .file_name()
- .to_str()
- .map_or(false, |name| name == "next-ls")
- {
- last_binary_path = Some(entry.path());
- }
- }
-
- if let Some(path) = last_binary_path {
- Ok(LanguageServerBinary {
- path,
- arguments: Vec::new(),
- })
- } else {
- Err(anyhow!("no cached binary"))
- }
- })
- .await
- .log_err()
-}
-
-pub struct LocalNextLspAdapter {
- pub path: String,
- pub arguments: Vec<String>,
-}
-
-#[async_trait]
-impl LspAdapter for LocalNextLspAdapter {
- async fn name(&self) -> LanguageServerName {
- LanguageServerName("local-next-ls".into())
- }
-
- fn short_name(&self) -> &'static str {
- "next-ls"
- }
-
- async fn fetch_latest_server_version(
- &self,
- _: &dyn LspAdapterDelegate,
- ) -> Result<Box<dyn 'static + Send + Any>> {
- Ok(Box::new(()) as Box<_>)
- }
-
- async fn fetch_server_binary(
- &self,
- _: Box<dyn 'static + Send + Any>,
- _: PathBuf,
- _: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- let path = shellexpand::full(&self.path)?;
- Ok(LanguageServerBinary {
- path: PathBuf::from(path.deref()),
- arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
- })
- }
-
- async fn cached_server_binary(
- &self,
- _: PathBuf,
- _: &dyn LspAdapterDelegate,
- ) -> Option<LanguageServerBinary> {
- let path = shellexpand::full(&self.path).ok()?;
- Some(LanguageServerBinary {
- path: PathBuf::from(path.deref()),
- arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
- })
- }
-
- async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
- let path = shellexpand::full(&self.path).ok()?;
- Some(LanguageServerBinary {
- path: PathBuf::from(path.deref()),
- arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
- })
- }
-
- async fn label_for_symbol(
- &self,
- name: &str,
- symbol: SymbolKind,
- language: &Arc<Language>,
- ) -> Option<CodeLabel> {
- label_for_symbol_next(name, symbol, language)
- }
-}
-
-fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc<Language>) -> Option<CodeLabel> {
- Some(CodeLabel {
- runs: language.highlight_text(&name.into(), 0..name.len()),
- text: name.to_string(),
- filter_range: 0..name.len(),
- })
-}
@@ -2,6 +2,7 @@
[(line_comment) (attribute_item)]* @context
.
[
+
(struct_item
name: (_) @name)
@@ -26,3 +27,6 @@
name: (_) @name)
] @item
)
+
+(attribute_item) @collapse
+(use_declaration) @collapse
@@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
use fs::RealFs;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::AppState;
+use workspace::{AppState, WorkspaceStore};
use zed::{
assets::Assets,
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -139,6 +139,7 @@ fn main() {
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
cx.set_global(client.clone());
@@ -176,7 +177,7 @@ fn main() {
})
.detach();
- client.telemetry().start(installation_id);
+ client.telemetry().start(installation_id, cx);
let app_state = Arc::new(AppState {
languages,
@@ -187,6 +188,7 @@ fn main() {
build_window_options,
initialize_workspace,
background_actions,
+ workspace_store,
});
cx.set_global(Arc::downgrade(&app_state));
@@ -44,6 +44,7 @@ position_2=${half_width},${y}
# Authenticate using the collab server's admin secret.
export ZED_STATELESS=1
+export ZED_ALWAYS_ACTIVE=1
export ZED_ADMIN_API_TOKEN=secret
export ZED_SERVER_URL=http://localhost:8080
export ZED_WINDOW_SIZE=${half_width},${height}
@@ -1,4 +1,4 @@
-import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component"
+import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component"
import { interactive, toggleable } from "../element"
import { useTheme, with_opacity } from "../theme"
import { background, border, foreground, text } from "./components"
@@ -191,6 +191,12 @@ export function titlebar(): any {
color: "variant",
}),
+ project_host: text_button({
+ text_properties: {
+ weight: "bold"
+ }
+ }),
+
// Collaborators
leader_avatar: {
width: avatar_width,