catching up with main

This commit is contained in:
KCaverly 2023-10-03 12:05:00 +03:00
commit 166ca2a227
98 changed files with 4732 additions and 3010 deletions

9
Cargo.lock generated
View file

@ -1416,6 +1416,7 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"sum_tree", "sum_tree",
"sysinfo",
"tempfile", "tempfile",
"text", "text",
"thiserror", "thiserror",
@ -2791,7 +2792,6 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
"lsp",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"regex", "regex",
"rope", "rope",
@ -7404,6 +7404,8 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"fs",
"futures 0.3.28",
"gpui2", "gpui2",
"itertools 0.11.0", "itertools 0.11.0",
"log", "log",
@ -7607,9 +7609,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.27.8" version = "0.29.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.3",
@ -8624,6 +8626,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"gpui2", "gpui2",
"rand 0.8.5",
"serde", "serde",
"settings", "settings",
"smallvec", "smallvec",

View file

@ -111,6 +111,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
smallvec = { version = "1.6", features = ["union"] } smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" } smol = { version = "1.2" }
sysinfo = "0.29.10"
tempdir = { version = "0.3.7" } tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" } thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }

View file

@ -1,4 +1,4 @@
web: cd ../zed.dev && PORT=3000 npx vercel dev web: cd ../zed.dev && PORT=3000 npm run dev
collab: cd crates/collab && cargo run serve collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
livekit: livekit-server --dev livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf postgrest: postgrest crates/collab/admin_api.conf

View file

@ -361,7 +361,7 @@
".venv", ".venv",
"venv" "venv"
], ],
// Can also be 'csh' and 'fish' // Can also be 'csh', 'fish', and `nushell`
"activate_script": "default" "activate_script": "default"
} }
} }
@ -379,24 +379,24 @@
}, },
// Settings specific to our elixir integration // Settings specific to our elixir integration
"elixir": { "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 // Note that changing this setting requires a restart of Zed
// to take effect. // to take effect.
// //
// May take 3 values: // May take 3 values:
// 1. Use the standard elixir-ls LSP server // 1. Use the standard ElixirLS, this is the default
// "next": "off" // "lsp": "elixir_ls"
// 2. Use a bundled version of the next Next LS LSP server // 2. Use the experimental NextLs
// "next": "on", // "lsp": "next_ls",
// 3. Use a local build of the next Next LS LSP server: // 3. Use a language server installed locally on your machine:
// "next": { // "lsp": {
// "local": { // "local": {
// "path": "~/next-ls/bin/start", // "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"] // "arguments": ["--stdio"]
// } // }
// }, // },
// //
"next": "off" "lsp": "elixir_ls"
}, },
// Different settings for specific languages. // Different settings for specific languages.
"languages": { "languages": {

View file

@ -1,6 +1,6 @@
use crate::codegen::CodegenKind; use crate::codegen::CodegenKind;
use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
use std::cmp; use std::cmp::{self, Reverse};
use std::fmt::Write; use std::fmt::Write;
use std::iter; use std::iter;
use std::ops::Range; use std::ops::Range;
@ -14,16 +14,16 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
} }
let selected_range = selected_range.to_offset(buffer); 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) Some(&grammar.embedding_config.as_ref()?.query)
}); });
let configs = matches let configs = ts_matches
.grammars() .grammars()
.iter() .iter()
.map(|g| g.embedding_config.as_ref().unwrap()) .map(|g| g.embedding_config.as_ref().unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut matches = iter::from_fn(move || { let mut matches = Vec::new();
while let Some(mat) = matches.peek() { while let Some(mat) = ts_matches.peek() {
let config = &configs[mat.grammar_index]; let config = &configs[mat.grammar_index];
if let Some(collapse) = mat.captures.iter().find_map(|cap| { if let Some(collapse) = mat.captures.iter().find_map(|cap| {
if Some(cap.index) == config.collapse_capture_ix { if Some(cap.index) == config.collapse_capture_ix {
@ -40,33 +40,32 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
continue; continue;
} }
} }
matches.advance(); ts_matches.advance();
return Some(Match { collapse, keep }); matches.push(Match { collapse, keep });
} else { } else {
matches.advance(); ts_matches.advance();
} }
} }
None matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
}) let mut matches = matches.into_iter().peekable();
.peekable();
let mut summary = String::new(); let mut summary = String::new();
let mut offset = 0; let mut offset = 0;
let mut flushed_selection = false; 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 // Keep extending the collapsed range if the next match surrounds
// the current one. // the current one.
while let Some(next_mat) = matches.peek() { while let Some(next_mat) = matches.peek() {
if next_mat.collapse.start <= mat.collapse.start if mat.collapse.start <= next_mat.collapse.start
&& next_mat.collapse.end >= mat.collapse.end && mat.collapse.end >= next_mat.collapse.end
{ {
mat = matches.next().unwrap(); matches.next().unwrap();
} else { } else {
break; break;
} }
} }
if offset >= mat.collapse.start { if offset > mat.collapse.start {
// Skip collapsed nodes that have already been summarized. // Skip collapsed nodes that have already been summarized.
offset = cmp::max(offset, mat.collapse.end); offset = cmp::max(offset, mat.collapse.end);
continue; continue;

View file

@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) { 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>() let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{ {
format!("{server_url}/releases/preview/latest") format!("{server_url}/releases/preview/{current_version}")
} else { } else {
format!("{server_url}/releases/stable/latest") format!("{server_url}/releases/stable/{current_version}")
}; };
cx.platform().open_url(&latest_release_url); cx.platform().open_url(&latest_release_url);
} }

View file

@ -2,22 +2,23 @@ pub mod call_settings;
pub mod participant; pub mod participant;
pub mod room; pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::Audio; use audio::Audio;
use call_settings::CallSettings; use call_settings::CallSettings;
use channel::ChannelId; 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 collections::HashSet;
use futures::{future::Shared, FutureExt}; use futures::{future::Shared, FutureExt};
use postage::watch;
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle, WeakModelHandle,
}; };
use postage::watch;
use project::Project; use project::Project;
use std::sync::Arc;
pub use participant::ParticipantLocation; pub use participant::ParticipantLocation;
pub use room::Room; pub use room::Room;
@ -68,6 +69,7 @@ impl ActiveCall {
location: None, location: None,
pending_invites: Default::default(), pending_invites: Default::default(),
incoming_call: watch::channel(), incoming_call: watch::channel(),
_subscriptions: vec![ _subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled), client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@ -348,18 +350,23 @@ impl ActiveCall {
} }
} }
pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
self.location.as_ref()
}
pub fn set_location( pub fn set_location(
&mut self, &mut self,
project: Option<&ModelHandle<Project>>, project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade()); self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() { if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx)) return room.update(cx, |room, cx| room.set_location(project, cx));
} else {
Task::ready(Ok(()))
} }
} }
Task::ready(Ok(()))
}
fn set_room( fn set_room(
&mut self, &mut self,

View file

@ -1,4 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use client::ParticipantIndex;
use client::{proto, User}; use client::{proto, User};
use collections::HashMap; use collections::HashMap;
use gpui::WeakModelHandle; use gpui::WeakModelHandle;
@ -43,6 +44,7 @@ pub struct RemoteParticipant {
pub peer_id: proto::PeerId, pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>, pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation, pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool, pub muted: bool,
pub speaking: bool, pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>, pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

View file

@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
use audio::{Audio, Sound}; use audio::{Audio, Sound};
use client::{ use client::{
proto::{self, PeerId}, proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore, Client, ParticipantIndex, TypedEnvelope, User, UserStore,
}; };
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs; use fs::Fs;
@ -44,6 +44,12 @@ pub enum Event {
RemoteProjectUnshared { RemoteProjectUnshared {
project_id: u64, project_id: u64,
}, },
RemoteProjectJoined {
project_id: u64,
},
RemoteProjectInvitationDiscarded {
project_id: u64,
},
Left, Left,
} }
@ -714,6 +720,9 @@ impl Room {
participant.user_id, participant.user_id,
RemoteParticipant { RemoteParticipant {
user: user.clone(), user: user.clone(),
participant_index: ParticipantIndex(
participant.participant_index,
),
peer_id, peer_id,
projects: participant.projects, projects: participant.projects,
location, location,
@ -807,6 +816,15 @@ impl Room {
let _ = this.leave(cx); 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(); this.check_invariants();
cx.notify(); cx.notify();
}); });
@ -1003,6 +1021,7 @@ impl Room {
) -> Task<Result<ModelHandle<Project>>> { ) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone(); let client = self.client.clone();
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let project = let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;

View file

@ -1,22 +1,25 @@
use crate::Channel; use crate::Channel;
use anyhow::Result; use anyhow::Result;
use client::Client; use client::{Client, Collaborator, UserStore};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
use rpc::{proto, TypedEnvelope}; use rpc::{
proto::{self, PeerId},
TypedEnvelope,
};
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
pub(crate) fn init(client: &Arc<Client>) { pub(crate) fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); 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_update_channel_buffer_collaborators);
client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
} }
pub struct ChannelBuffer { pub struct ChannelBuffer {
pub(crate) channel: Arc<Channel>, pub(crate) channel: Arc<Channel>,
connected: bool, connected: bool,
collaborators: Vec<proto::Collaborator>, collaborators: HashMap<PeerId, Collaborator>,
user_store: ModelHandle<UserStore>,
buffer: ModelHandle<language::Buffer>, buffer: ModelHandle<language::Buffer>,
buffer_epoch: u64, buffer_epoch: u64,
client: Arc<Client>, client: Arc<Client>,
@ -46,6 +49,7 @@ impl ChannelBuffer {
pub(crate) async fn new( pub(crate) async fn new(
channel: Arc<Channel>, channel: Arc<Channel>,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> { ) -> Result<ModelHandle<Self>> {
let response = client let response = client
@ -61,8 +65,6 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation) .map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let collaborators = response.collaborators;
let buffer = cx.add_model(|_| { let buffer = cx.add_model(|_| {
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) 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| { anyhow::Ok(cx.add_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach(); cx.subscribe(&buffer, Self::on_buffer_update).detach();
Self { let mut this = Self {
buffer, buffer,
buffer_epoch: response.epoch, buffer_epoch: response.epoch,
client, client,
connected: true, connected: true,
collaborators, collaborators: Default::default(),
channel, channel,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), 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( pub(crate) fn replace_collaborators(
&mut self, &mut self,
collaborators: Vec<proto::Collaborator>, collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
for old_collaborator in &self.collaborators { let mut new_collaborators = HashMap::default();
if collaborators for collaborator in collaborators {
.iter() if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
.any(|c| c.replica_id == old_collaborator.replica_id) 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| { self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id as u16, cx) buffer.remove_peer(old_collaborator.replica_id as u16, cx)
}); });
} }
} }
self.collaborators = collaborators; self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify(); cx.notify();
} }
@ -127,64 +140,14 @@ impl ChannelBuffer {
Ok(()) Ok(())
} }
async fn handle_add_channel_buffer_collaborator( async fn handle_update_channel_buffer_collaborators(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>, message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
_: 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(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
_: Arc<Client>, _: Arc<Client>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<()> { ) -> Result<()> {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.collaborators.retain(|collaborator| { this.replace_collaborators(message.payload.collaborators, cx);
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;
}
}
cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify(); cx.notify();
}); });
@ -217,7 +180,7 @@ impl ChannelBuffer {
self.buffer.clone() self.buffer.clone()
} }
pub fn collaborators(&self) -> &[proto::Collaborator] { pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators &self.collaborators
} }

View file

@ -198,10 +198,11 @@ impl ChannelStore {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<ChannelBuffer>>> { ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
let client = self.client.clone(); let client = self.client.clone();
let user_store = self.user_store.clone();
self.open_channel_resource( self.open_channel_resource(
channel_id, channel_id,
|this| &mut this.opened_buffers, |this| &mut this.opened_buffers,
|channel, cx| ChannelBuffer::new(channel, client, cx), |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
cx, cx,
) )
} }

View file

@ -33,15 +33,16 @@ parking_lot.workspace = true
postage.workspace = true postage.workspace = true
rand.workspace = true rand.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true smol.workspace = true
sysinfo.workspace = true
tempfile = "3"
thiserror.workspace = true thiserror.workspace = true
time.workspace = true time.workspace = true
tiny_http = "0.8" tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2" url = "2.2"
serde.workspace = true uuid = { version = "1.1.2", features = ["v4"] }
serde_derive.workspace = true
tempfile = "3"
[dev-dependencies] [dev-dependencies]
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }

View file

@ -62,6 +62,8 @@ lazy_static! {
.and_then(|v| v.parse().ok()); .and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> = pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); 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"; pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";

View file

@ -4,6 +4,7 @@ use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Serialize; use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use util::http::HttpClient; use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt}; use util::{channel::ReleaseChannel, TryFutureExt};
@ -88,6 +89,16 @@ pub enum ClickhouseEvent {
kind: AssistantKind, kind: AssistantKind,
model: &'static str, 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)] #[cfg(debug_assertions)]
@ -136,7 +147,7 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) 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(); let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into()); state.installation_id = installation_id.map(|id| id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
@ -145,6 +156,48 @@ impl Telemetry {
if has_clickhouse_events { if has_clickhouse_events {
self.flush_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(&current_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( pub fn set_authenticated_user_info(

View file

@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch}; use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse}; use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::http::HttpClient; use util::http::HttpClient;
use util::TryFutureExt as _; use util::TryFutureExt as _;
pub type UserId = u64; pub type UserId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct User { pub struct User {
pub id: UserId, pub id: UserId,
@ -19,6 +23,13 @@ pub struct User {
pub avatar: Option<Arc<ImageData>>, 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 { impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
pub struct UserStore { pub struct UserStore {
users: HashMap<u64, Arc<User>>, users: HashMap<u64, Arc<User>>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>, update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>, current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>, contacts: Vec<Arc<Contact>>,
@ -81,6 +93,7 @@ pub enum Event {
kind: ContactEventKind, kind: ContactEventKind,
}, },
ShowContacts, ShowContacts,
ParticipantIndicesChanged,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -118,6 +131,7 @@ impl UserStore {
current_user: current_user_rx, current_user: current_user_rx,
contacts: Default::default(), contacts: Default::default(),
incoming_contact_requests: Default::default(), incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
outgoing_contact_requests: Default::default(), outgoing_contact_requests: Default::default(),
invite_info: None, invite_info: None,
client: Arc::downgrade(&client), client: Arc::downgrade(&client),
@ -581,6 +595,10 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx) 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( pub fn get_user(
&mut self, &mut self,
user_id: u64, 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 { 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>> { async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let mut response = http let mut response = http
.get(url, Default::default(), true) .get(url, Default::default(), true)

View file

@ -158,7 +158,8 @@ CREATE TABLE "room_participants" (
"initial_project_id" INTEGER, "initial_project_id" INTEGER,
"calling_user_id" INTEGER NOT NULL REFERENCES users (id), "calling_user_id" INTEGER NOT NULL REFERENCES users (id),
"calling_connection_id" INTEGER NOT NULL, "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 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"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");

View file

@ -0,0 +1 @@
ALTER TABLE room_participants ADD COLUMN participant_index INTEGER;

View file

@ -510,7 +510,7 @@ pub struct RefreshedRoom {
pub struct RefreshedChannelBuffer { pub struct RefreshedChannelBuffer {
pub connection_ids: Vec<ConnectionId>, pub connection_ids: Vec<ConnectionId>,
pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>, pub collaborators: Vec<proto::Collaborator>,
} }
pub struct Project { pub struct Project {

View file

@ -2,6 +2,12 @@ use super::*;
use prost::Message; use prost::Message;
use text::{EditOperation, UndoOperation}; use text::{EditOperation, UndoOperation};
pub struct LeftChannelBuffer {
pub channel_id: ChannelId,
pub collaborators: Vec<proto::Collaborator>,
pub connections: Vec<ConnectionId>,
}
impl Database { impl Database {
pub async fn join_channel_buffer( pub async fn join_channel_buffer(
&self, &self,
@ -204,23 +210,26 @@ impl Database {
server_id: ServerId, server_id: ServerId,
) -> Result<RefreshedChannelBuffer> { ) -> Result<RefreshedChannelBuffer> {
self.transaction(|tx| async move { 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)) .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
.all(&*tx) .all(&*tx)
.await?; .await?;
let mut connection_ids = Vec::new(); 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(); let mut collaborator_ids_to_remove = Vec::new();
for collaborator in &collaborators { for db_collaborator in &db_collaborators {
if !collaborator.connection_lost && collaborator.connection_server_id == server_id { if !db_collaborator.connection_lost
connection_ids.push(collaborator.connection()); && 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 { } else {
removed_collaborators.push(proto::RemoveChannelBufferCollaborator { collaborator_ids_to_remove.push(db_collaborator.id);
channel_id: channel_id.to_proto(),
peer_id: Some(collaborator.connection().into()),
});
collaborator_ids_to_remove.push(collaborator.id);
} }
} }
@ -231,7 +240,7 @@ impl Database {
Ok(RefreshedChannelBuffer { Ok(RefreshedChannelBuffer {
connection_ids, connection_ids,
removed_collaborators, collaborators,
}) })
}) })
.await .await
@ -241,7 +250,7 @@ impl Database {
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
connection: ConnectionId, connection: ConnectionId,
) -> Result<Vec<ConnectionId>> { ) -> Result<LeftChannelBuffer> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
self.leave_channel_buffer_internal(channel_id, connection, &*tx) self.leave_channel_buffer_internal(channel_id, connection, &*tx)
.await .await
@ -275,7 +284,7 @@ impl Database {
pub async fn leave_channel_buffers( pub async fn leave_channel_buffers(
&self, &self,
connection: ConnectionId, connection: ConnectionId,
) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> { ) -> Result<Vec<LeftChannelBuffer>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryChannelIds { enum QueryChannelIds {
@ -294,10 +303,10 @@ impl Database {
let mut result = Vec::new(); let mut result = Vec::new();
for channel_id in channel_ids { for channel_id in channel_ids {
let collaborators = self let left_channel_buffer = self
.leave_channel_buffer_internal(channel_id, connection, &*tx) .leave_channel_buffer_internal(channel_id, connection, &*tx)
.await?; .await?;
result.push((channel_id, collaborators)); result.push(left_channel_buffer);
} }
Ok(result) Ok(result)
@ -310,7 +319,7 @@ impl Database {
channel_id: ChannelId, channel_id: ChannelId,
connection: ConnectionId, connection: ConnectionId,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Vec<ConnectionId>> { ) -> Result<LeftChannelBuffer> {
let result = channel_buffer_collaborator::Entity::delete_many() let result = channel_buffer_collaborator::Entity::delete_many()
.filter( .filter(
Condition::all() Condition::all()
@ -327,6 +336,7 @@ impl Database {
Err(anyhow!("not a collaborator on this project"))?; Err(anyhow!("not a collaborator on this project"))?;
} }
let mut collaborators = Vec::new();
let mut connections = Vec::new(); let mut connections = Vec::new();
let mut rows = channel_buffer_collaborator::Entity::find() let mut rows = channel_buffer_collaborator::Entity::find()
.filter( .filter(
@ -336,19 +346,26 @@ impl Database {
.await?; .await?;
while let Some(row) = rows.next().await { while let Some(row) = rows.next().await {
let row = row?; let row = row?;
connections.push(ConnectionId { let connection = row.connection();
id: row.connection_id as u32, connections.push(connection);
owner_id: row.connection_server_id.0 as u32, 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); drop(rows);
if connections.is_empty() { if collaborators.is_empty() {
self.snapshot_channel_buffer(channel_id, &tx).await?; self.snapshot_channel_buffer(channel_id, &tx).await?;
} }
Ok(connections) Ok(LeftChannelBuffer {
channel_id,
collaborators,
connections,
})
} }
pub async fn get_channel_buffer_collaborators( pub async fn get_channel_buffer_collaborators(

View file

@ -738,7 +738,7 @@ impl Database {
Condition::any() Condition::any()
.add( .add(
Condition::all() Condition::all()
.add(follower::Column::ProjectId.eq(project_id)) .add(follower::Column::ProjectId.eq(Some(project_id)))
.add( .add(
follower::Column::LeaderConnectionServerId follower::Column::LeaderConnectionServerId
.eq(connection.owner_id), .eq(connection.owner_id),
@ -747,7 +747,7 @@ impl Database {
) )
.add( .add(
Condition::all() Condition::all()
.add(follower::Column::ProjectId.eq(project_id)) .add(follower::Column::ProjectId.eq(Some(project_id)))
.add( .add(
follower::Column::FollowerConnectionServerId follower::Column::FollowerConnectionServerId
.eq(connection.owner_id), .eq(connection.owner_id),
@ -862,13 +862,46 @@ impl Database {
.await .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( pub async fn follow(
&self, &self,
room_id: RoomId,
project_id: ProjectId, project_id: ProjectId,
leader_connection: ConnectionId, leader_connection: ConnectionId,
follower_connection: ConnectionId, follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> { ) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move { self.room_transaction(room_id, |tx| async move {
follower::ActiveModel { follower::ActiveModel {
room_id: ActiveValue::set(room_id), room_id: ActiveValue::set(room_id),
@ -894,15 +927,16 @@ impl Database {
pub async fn unfollow( pub async fn unfollow(
&self, &self,
room_id: RoomId,
project_id: ProjectId, project_id: ProjectId,
leader_connection: ConnectionId, leader_connection: ConnectionId,
follower_connection: ConnectionId, follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> { ) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move { self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many() follower::Entity::delete_many()
.filter( .filter(
Condition::all() Condition::all()
.add(follower::Column::RoomId.eq(room_id))
.add(follower::Column::ProjectId.eq(project_id)) .add(follower::Column::ProjectId.eq(project_id))
.add( .add(
follower::Column::LeaderConnectionServerId follower::Column::LeaderConnectionServerId

View file

@ -128,6 +128,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId( calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32, connection.owner_id as i32,
))), ))),
participant_index: ActiveValue::set(Some(0)),
..Default::default() ..Default::default()
} }
.insert(&*tx) .insert(&*tx)
@ -152,6 +153,7 @@ impl Database {
room_id: ActiveValue::set(room_id), room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(called_user_id), user_id: ActiveValue::set(called_user_id),
answering_connection_lost: ActiveValue::set(false), answering_connection_lost: ActiveValue::set(false),
participant_index: ActiveValue::NotSet,
calling_user_id: ActiveValue::set(calling_user_id), calling_user_id: ActiveValue::set(calling_user_id),
calling_connection_id: ActiveValue::set(calling_connection.id as i32), calling_connection_id: ActiveValue::set(calling_connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId( calling_connection_server_id: ActiveValue::set(Some(ServerId(
@ -283,6 +285,26 @@ impl Database {
.await? .await?
.ok_or_else(|| anyhow!("no such room"))?; .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 { if let Some(channel_id) = channel_id {
self.check_user_is_channel_member(channel_id, user_id, &*tx) self.check_user_is_channel_member(channel_id, user_id, &*tx)
.await?; .await?;
@ -300,6 +322,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId( calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32, connection.owner_id as i32,
))), ))),
participant_index: ActiveValue::Set(Some(participant_index)),
..Default::default() ..Default::default()
}]) }])
.on_conflict( .on_conflict(
@ -308,6 +331,7 @@ impl Database {
room_participant::Column::AnsweringConnectionId, room_participant::Column::AnsweringConnectionId,
room_participant::Column::AnsweringConnectionServerId, room_participant::Column::AnsweringConnectionServerId,
room_participant::Column::AnsweringConnectionLost, room_participant::Column::AnsweringConnectionLost,
room_participant::Column::ParticipantIndex,
]) ])
.to_owned(), .to_owned(),
) )
@ -322,6 +346,7 @@ impl Database {
.add(room_participant::Column::AnsweringConnectionId.is_null()), .add(room_participant::Column::AnsweringConnectionId.is_null()),
) )
.set(room_participant::ActiveModel { .set(room_participant::ActiveModel {
participant_index: ActiveValue::Set(Some(participant_index)),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId( answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32, connection.owner_id as i32,
@ -960,6 +985,39 @@ impl Database {
Ok(room) 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( async fn get_channel_room(
&self, &self,
room_id: RoomId, room_id: RoomId,
@ -978,10 +1036,15 @@ impl Database {
let mut pending_participants = Vec::new(); let mut pending_participants = Vec::new();
while let Some(db_participant) = db_participants.next().await { while let Some(db_participant) = db_participants.next().await {
let db_participant = db_participant?; let db_participant = db_participant?;
if let Some((answering_connection_id, answering_connection_server_id)) = db_participant if let (
.answering_connection_id Some(answering_connection_id),
.zip(db_participant.answering_connection_server_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 ( let location = match (
db_participant.location_kind, db_participant.location_kind,
db_participant.location_project_id, db_participant.location_project_id,
@ -1012,6 +1075,7 @@ impl Database {
peer_id: Some(answering_connection.into()), peer_id: Some(answering_connection.into()),
projects: Default::default(), projects: Default::default(),
location: Some(proto::ParticipantLocation { variant: location }), location: Some(proto::ParticipantLocation { variant: location }),
participant_index: participant_index as u32,
}, },
); );
} else { } else {

View file

@ -1,4 +1,5 @@
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@ -17,6 +18,16 @@ pub struct Model {
pub calling_user_id: UserId, pub calling_user_id: UserId,
pub calling_connection_id: i32, pub calling_connection_id: i32,
pub calling_connection_server_id: Option<ServerId>, 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)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
assert_eq!(zed_collaborats, &[a_id, b_id]); assert_eq!(zed_collaborats, &[a_id, b_id]);
let collaborators = db let left_buffer = db
.leave_channel_buffer(zed_id, connection_id_b) .leave_channel_buffer(zed_id, connection_id_b)
.await .await
.unwrap(); .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 cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
let _ = db let _ = db

View file

@ -38,8 +38,8 @@ use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge}; use prometheus::{register_int_gauge, IntGauge};
use rpc::{ use rpc::{
proto::{ proto::{
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage, self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
}, },
Connection, ConnectionId, Peer, Receipt, TypedEnvelope, Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
}; };
@ -313,9 +313,16 @@ impl Server {
.trace_err() .trace_err()
{ {
for connection_id in refreshed_channel_buffer.connection_ids { for connection_id in refreshed_channel_buffer.connection_ids {
for message in &refreshed_channel_buffer.removed_collaborators { peer.send(
peer.send(connection_id, message.clone()).trace_err(); connection_id,
} proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
collaborators: refreshed_channel_buffer
.collaborators
.clone(),
},
)
.trace_err();
} }
} }
} }
@ -1883,25 +1890,20 @@ async fn follow(
response: Response<proto::Follow>, response: Response<proto::Follow>,
session: Session, session: Session,
) -> Result<()> { ) -> 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 let leader_id = request
.leader_id .leader_id
.ok_or_else(|| anyhow!("invalid leader id"))? .ok_or_else(|| anyhow!("invalid leader id"))?
.into(); .into();
let follower_id = session.connection_id; let follower_id = session.connection_id;
{ session
let project_connection_ids = session
.db() .db()
.await .await
.project_connection_ids(project_id, session.connection_id) .check_room_participants(room_id, leader_id, session.connection_id)
.await?; .await?;
if !project_connection_ids.contains(&leader_id) {
Err(anyhow!("no such peer"))?;
}
}
let mut response_payload = session let mut response_payload = session
.peer .peer
.forward_request(session.connection_id, leader_id, request) .forward_request(session.connection_id, leader_id, request)
@ -1911,56 +1913,63 @@ async fn follow(
.retain(|view| view.leader_id != Some(follower_id.into())); .retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?; response.send(response_payload)?;
if let Some(project_id) = project_id {
let room = session let room = session
.db() .db()
.await .await
.follow(project_id, leader_id, follower_id) .follow(room_id, project_id, leader_id, follower_id)
.await?; .await?;
room_updated(&room, &session.peer); room_updated(&room, &session.peer);
}
Ok(()) Ok(())
} }
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { 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 let leader_id = request
.leader_id .leader_id
.ok_or_else(|| anyhow!("invalid leader id"))? .ok_or_else(|| anyhow!("invalid leader id"))?
.into(); .into();
let follower_id = session.connection_id; let follower_id = session.connection_id;
if !session session
.db() .db()
.await .await
.project_connection_ids(project_id, session.connection_id) .check_room_participants(room_id, leader_id, session.connection_id)
.await? .await?;
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
session session
.peer .peer
.forward_send(session.connection_id, leader_id, request)?; .forward_send(session.connection_id, leader_id, request)?;
if let Some(project_id) = project_id {
let room = session let room = session
.db() .db()
.await .await
.unfollow(project_id, leader_id, follower_id) .unfollow(room_id, project_id, leader_id, follower_id)
.await?; .await?;
room_updated(&room, &session.peer); room_updated(&room, &session.peer);
}
Ok(()) Ok(())
} }
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id); let room_id = RoomId::from_proto(request.room_id);
let project_connection_ids = session let database = session.db.lock().await;
.db
.lock() let connection_ids = if let Some(project_id) = request.project_id {
.await let project_id = ProjectId::from_proto(project_id);
database
.project_connection_ids(project_id, session.connection_id) .project_connection_ids(project_id, session.connection_id)
.await?; .await?
} else {
database
.room_connection_ids(room_id, session.connection_id)
.await?
};
let leader_id = request.variant.as_ref().and_then(|variant| match variant { let leader_id = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::CreateView(payload) => payload.leader_id, 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() { for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into(); let follower_connection_id = follower_peer_id.into();
if project_connection_ids.contains(&follower_connection_id) if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
&& Some(follower_peer_id) != leader_id
{
session.peer.forward_send( session.peer.forward_send(
session.connection_id, session.connection_id,
follower_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) .join_channel_buffer(channel_id, session.user_id, session.connection_id)
.await?; .await?;
let replica_id = open_response.replica_id;
let collaborators = open_response.collaborators.clone(); let collaborators = open_response.collaborators.clone();
response.send(open_response)?; response.send(open_response)?;
let update = AddChannelBufferCollaborator { let update = UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(), channel_id: channel_id.to_proto(),
collaborator: Some(proto::Collaborator { collaborators: collaborators.clone(),
user_id: session.user_id.to_proto(),
peer_id: Some(session.connection_id.into()),
replica_id,
}),
}; };
channel_buffer_updated( channel_buffer_updated(
session.connection_id, session.connection_id,
@ -2716,8 +2717,8 @@ async fn rejoin_channel_buffers(
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
.await?; .await?;
for buffer in &buffers { for rejoined_buffer in &buffers {
let collaborators_to_notify = buffer let collaborators_to_notify = rejoined_buffer
.buffer .buffer
.collaborators .collaborators
.iter() .iter()
@ -2725,10 +2726,9 @@ async fn rejoin_channel_buffers(
channel_buffer_updated( channel_buffer_updated(
session.connection_id, session.connection_id,
collaborators_to_notify, collaborators_to_notify,
&proto::UpdateChannelBufferCollaborator { &proto::UpdateChannelBufferCollaborators {
channel_id: buffer.buffer.channel_id, channel_id: rejoined_buffer.buffer.channel_id,
old_peer_id: Some(buffer.old_connection_id.into()), collaborators: rejoined_buffer.buffer.collaborators.clone(),
new_peer_id: Some(session.connection_id.into()),
}, },
&session.peer, &session.peer,
); );
@ -2749,7 +2749,7 @@ async fn leave_channel_buffer(
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); 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) .leave_channel_buffer(channel_id, session.connection_id)
.await?; .await?;
@ -2757,10 +2757,10 @@ async fn leave_channel_buffer(
channel_buffer_updated( channel_buffer_updated(
session.connection_id, session.connection_id,
collaborators_to_notify, left_buffer.connections,
&proto::RemoveChannelBufferCollaborator { &proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(), channel_id: channel_id.to_proto(),
peer_id: Some(session.connection_id.into()), collaborators: left_buffer.collaborators,
}, },
&session.peer, &session.peer,
); );
@ -3235,13 +3235,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
.leave_channel_buffers(session.connection_id) .leave_channel_buffers(session.connection_id)
.await?; .await?;
for (channel_id, connections) in left_channel_buffers { for left_buffer in left_channel_buffers {
channel_buffer_updated( channel_buffer_updated(
session.connection_id, session.connection_id,
connections, left_buffer.connections,
&proto::RemoveChannelBufferCollaborator { &proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(), channel_id: left_buffer.channel_id.to_proto(),
peer_id: Some(session.connection_id.into()), collaborators: left_buffer.collaborators,
}, },
&session.peer, &session.peer,
); );

View file

@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
mod channel_buffer_tests; mod channel_buffer_tests;
mod channel_message_tests; mod channel_message_tests;
mod channel_tests; mod channel_tests;
mod following_tests;
mod integration_tests; mod integration_tests;
mod random_channel_buffer_tests; mod random_channel_buffer_tests;
mod random_project_collaboration_tests; mod random_project_collaboration_tests;

View file

@ -4,14 +4,16 @@ use crate::{
}; };
use call::ActiveCall; use call::ActiveCall;
use channel::Channel; use channel::Channel;
use client::UserId; use client::ParticipantIndex;
use client::{Collaborator, UserId};
use collab_ui::channel_view::ChannelView; use collab_ui::channel_view::ChannelView;
use collections::HashMap; use collections::HashMap;
use editor::{Anchor, Editor, ToOffset};
use futures::future; use futures::future;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use rpc::{proto, RECEIVE_TIMEOUT}; use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::{ops::Range, sync::Arc};
#[gpui::test] #[gpui::test]
async fn test_core_channel_buffers( async fn test_core_channel_buffers(
@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
channel_buffer_b.read_with(cx_b, |buffer, _| { channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators( assert_collaborators(
&buffer.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] #[gpui::test]
async fn test_channel_buffer_replica_ids( async fn test_channel_notes_participant_indices(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext, mut cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext, mut cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext, cx_c: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); 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_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").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 let channel_id = server
.make_channel( .make_channel(
"the-channel", "the-channel",
@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
) )
.await; .await;
let active_call_a = cx_a.read(ActiveCall::global); client_a
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))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, 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() .fs()
.insert_tree("/dir", json!({ "file.txt": "contents" })) .insert_tree("/root", json!({"file.txt": "123"}))
.await; .await;
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
let shared_project_id = active_call_b let project_b = client_b.build_empty_local_project(cx_b);
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) 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_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
.await .await
.unwrap(); .unwrap();
// Client A joins the project // Clients A, B, and C all insert and select some text
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; 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]);
});
});
});
deterministic.run_until_parked(); deterministic.run_until_parked();
channel_view_b.update(cx_b, |notes, cx| {
// Client C is in a separate project. notes.editor.update(cx, |editor, cx| {
client_c.fs().insert_tree("/dir", json!({})).await; editor.move_down(&Default::default(), cx);
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; editor.insert("b", cx);
editor.change_selections(None, cx, |selections| {
// Note that each user has a different replica id in the projects vs the selections.select_ranges(vec![1..2]);
// 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); 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]);
}); });
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);
}); });
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);
}); });
let channel_window_a = // Client A sees clients B and C without assigned colors, because they aren't
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); // in a call together.
let channel_window_b = deterministic.run_until_parked();
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); channel_view_a.update(cx_a, |notes, cx| {
let channel_window_c = cx_c.add_window(|cx| { notes.editor.update(cx, |editor, cx| {
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
});
}); });
let channel_view_a = channel_window_a.root(cx_a); // Clients A and B join the same call.
let channel_view_b = channel_window_b.root(cx_b); for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
let channel_view_c = channel_window_c.root(cx_c); call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
// 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<_, _>>()
);
});
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>>(),
)
});
// 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 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))
.await .await
.unwrap(); .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);
});
// For clients A and B, client C's replica id in the channel buffer is // Clients A and B see each other with two different assigned colors. Client C
// now mapped to their replica id in the shared project. // still doesn't have a color.
channel_view_a.read_with(cx_a, |view, cx| { deterministic.run_until_parked();
assert_eq!( channel_view_a.update(cx_a, |notes, cx| {
view.editor.read(cx).replica_id_map().unwrap(), notes.editor.update(cx, |editor, cx| {
&[(1, 0), (2, 1), (0, 2)] assert_remote_selections(
.into_iter() editor,
.collect::<HashMap<_, _>>() &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
cx,
); );
}); });
channel_view_b.read_with(cx_b, |view, cx| { });
assert_eq!( channel_view_b.update(cx_b, |notes, cx| {
view.editor.read(cx).replica_id_map().unwrap(), notes.editor.update(cx, |editor, cx| {
&[(1, 0), (2, 1), (0, 2)] assert_remote_selections(
.into_iter() editor,
.collect::<HashMap<_, _>>(), &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
) cx,
);
});
});
// 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);
// 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 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();
// 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);
});
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] #[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(); deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").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_a.read_with(cx_a, |buffer_a, _| {
channel_buffer_b.read_with(cx_b, |buffer_b, _| { channel_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!( assert_collaborators(
buffer_a buffer_a.collaborators(),
.collaborators() &[client_a.user_id(), client_b.user_id()],
.iter()
.map(|c| c.user_id)
.collect::<Vec<_>>(),
vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
); );
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
}); });
}); });
} }
#[track_caller] #[gpui::test(iterations = 10)]
fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) { 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!( assert_eq!(
collaborators workspace.leader_for_pane(workspace.active_pane()),
.into_iter() 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: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
let mut user_ids = collaborators
.values()
.map(|collaborator| collaborator.user_id) .map(|collaborator| collaborator.user_id)
.collect::<Vec<_>>(), .collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
user_ids,
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>() ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
); );
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest {
// channel buffer. // channel buffer.
let collaborators = channel_buffer.collaborators(); let collaborators = channel_buffer.collaborators();
let mut user_ids = 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(); user_ids.sort();
assert_eq!( assert_eq!(
user_ids, user_ids,

View file

@ -29,7 +29,7 @@ use std::{
}, },
}; };
use util::http::FakeHttpClient; use util::http::FakeHttpClient;
use workspace::Workspace; use workspace::{Workspace, WorkspaceStore};
pub struct TestServer { pub struct TestServer {
pub app_state: Arc<AppState>, pub app_state: Arc<AppState>,
@ -204,13 +204,17 @@ impl TestServer {
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); 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 = let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); 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 { let app_state = Arc::new(workspace::AppState {
client: client.clone(), client: client.clone(),
user_store: user_store.clone(), user_store: user_store.clone(),
workspace_store,
channel_store: channel_store.clone(), channel_store: channel_store.clone(),
languages: Arc::new(LanguageRegistry::test()), languages: Arc::new(language_registry),
fs: fs.clone(), fs: fs.clone(),
build_window_options: |_, _, _| Default::default(), build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())), initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@ -536,15 +540,7 @@ impl TestClient {
root_path: impl AsRef<Path>, root_path: impl AsRef<Path>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> (ModelHandle<Project>, WorktreeId) { ) -> (ModelHandle<Project>, WorktreeId) {
let project = cx.update(|cx| { let project = self.build_empty_local_project(cx);
Project::local(
self.client().clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
cx,
)
});
let (worktree, _) = project let (worktree, _) = project
.update(cx, |p, cx| { .update(cx, |p, cx| {
p.find_or_create_local_worktree(root_path, true, 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())) (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( pub async fn build_remote_project(
&self, &self,
host_project_id: u64, host_project_id: u64,

View file

@ -1,10 +1,12 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use call::report_call_event_for_channel; use call::report_call_event_for_channel;
use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId}; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
use client::proto; use client::{
use clock::ReplicaId; proto::{self, PeerId},
Collaborator, ParticipantIndex,
};
use collections::HashMap; use collections::HashMap;
use editor::Editor; use editor::{CollaborationHub, Editor};
use gpui::{ use gpui::{
actions, actions,
elements::{ChildView, Label}, elements::{ChildView, Label},
@ -13,7 +15,11 @@ use gpui::{
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
use project::Project; use project::Project;
use std::any::{Any, TypeId}; use std::{
any::{Any, TypeId},
sync::Arc,
};
use util::ResultExt;
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemHandle}, item::{FollowableItem, Item, ItemHandle},
register_followable_item, register_followable_item,
@ -23,7 +29,7 @@ use workspace::{
actions!(channel_view, [Deploy]); actions!(channel_view, [Deploy]);
pub(crate) fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx) register_followable_item::<ChannelView>(cx)
} }
@ -36,9 +42,13 @@ pub struct ChannelView {
} }
impl 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 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 { cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?; let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| { pane.update(&mut cx, |pane, cx| {
@ -48,14 +58,13 @@ impl ChannelView {
&workspace.read(cx).app_state().client, &workspace.read(cx).app_state().client,
cx, 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, channel_id: ChannelId,
pane: ViewHandle<Pane>, pane: ViewHandle<Pane>,
workspace: ViewHandle<Workspace>, workspace: ViewHandle<Workspace>,
@ -74,12 +83,13 @@ impl ChannelView {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let channel_buffer = channel_buffer.await?; let channel_buffer = channel_buffer.await?;
let markdown = markdown.await?; if let Some(markdown) = markdown.await.log_err() {
channel_buffer.update(&mut cx, |buffer, cx| { channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx); buffer.set_language(Some(markdown), cx);
}) })
}); });
}
pane.update(&mut cx, |pane, cx| { pane.update(&mut cx, |pane, cx| {
pane.items_of_type::<Self>() pane.items_of_type::<Self>()
@ -96,40 +106,29 @@ impl ChannelView {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let buffer = channel_buffer.read(cx).buffer(); 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())); 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) cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
.detach(); .detach();
let this = Self { Self {
editor, editor,
project, project,
channel_buffer, channel_buffer,
remote_id: None, remote_id: None,
_editor_event_subscription, _editor_event_subscription,
}; }
this.refresh_replica_id_map(cx);
this
} }
fn handle_project_event( pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
&mut self, self.channel_buffer.read(cx).channel()
_: 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);
} }
fn handle_channel_buffer_event( fn handle_channel_buffer_event(
@ -138,50 +137,12 @@ impl ChannelView {
event: &ChannelBufferEvent, event: &ChannelBufferEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { if let ChannelBufferEvent::Disconnected = event {
ChannelBufferEvent::CollaboratorsChanged => { self.editor.update(cx, |editor, cx| {
self.refresh_replica_id_map(cx);
}
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
editor.set_read_only(true); editor.set_read_only(true);
cx.notify(); 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)
});
} }
} }
@ -311,7 +272,7 @@ impl FollowableItem for ChannelView {
unreachable!() 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 { Some(cx.spawn(|mut cx| async move {
let this = open.await?; let this = open.await?;
@ -371,17 +332,32 @@ impl FollowableItem for ChannelView {
}) })
} }
fn set_leader_replica_id( fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
&mut self,
leader_replica_id: Option<u16>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| { 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 { fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
Editor::should_unfollow_on_event(event, cx) 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()
}
} }

View file

@ -409,7 +409,7 @@ impl ChatPanel {
}) })
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, _, cx| {
if let Some(workspace) = workspace.upgrade(cx) { if let Some(workspace) = workspace.upgrade(cx) {
ChannelView::deploy(channel_id, workspace, cx); ChannelView::open(channel_id, workspace, cx).detach();
} }
}) })
.with_tooltip::<OpenChannelNotes>( .with_tooltip::<OpenChannelNotes>(
@ -546,7 +546,7 @@ impl ChatPanel {
if let Some((chat, _)) = &self.active_chat { if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel().id; let channel_id = chat.read(cx).channel().id;
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
ChannelView::deploy(channel_id, workspace, cx); ChannelView::open(channel_id, workspace, cx).detach();
} }
} }
} }

View file

@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
item::ItemHandle, item::ItemHandle,
Workspace, FollowNextCollaborator, Workspace,
}; };
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -404,6 +404,7 @@ enum ListEntry {
Header(Section), Header(Section),
CallParticipant { CallParticipant {
user: Arc<User>, user: Arc<User>,
peer_id: Option<PeerId>,
is_pending: bool, is_pending: bool,
}, },
ParticipantProject { ParticipantProject {
@ -508,14 +509,19 @@ impl CollabPanel {
let is_collapsed = this.collapsed_sections.contains(section); let is_collapsed = this.collapsed_sections.contains(section);
this.render_header(*section, &theme, is_selected, is_collapsed, cx) this.render_header(*section, &theme, is_selected, is_collapsed, cx)
} }
ListEntry::CallParticipant { user, is_pending } => { ListEntry::CallParticipant {
Self::render_call_participant(
user, user,
peer_id,
is_pending,
} => Self::render_call_participant(
user,
*peer_id,
this.user_store.clone(),
*is_pending, *is_pending,
is_selected, is_selected,
&theme.collab_panel, &theme,
) cx,
} ),
ListEntry::ParticipantProject { ListEntry::ParticipantProject {
project_id, project_id,
worktree_root_names, worktree_root_names,
@ -528,7 +534,7 @@ impl CollabPanel {
Some(*project_id) == current_project_id, Some(*project_id) == current_project_id,
*is_last, *is_last,
is_selected, is_selected,
&theme.collab_panel, &theme,
cx, cx,
), ),
ListEntry::ParticipantScreen { peer_id, is_last } => { ListEntry::ParticipantScreen { peer_id, is_last } => {
@ -793,6 +799,7 @@ impl CollabPanel {
let user_id = user.id; let user_id = user.id;
self.entries.push(ListEntry::CallParticipant { self.entries.push(ListEntry::CallParticipant {
user, user,
peer_id: None,
is_pending: false, is_pending: false,
}); });
let mut projects = room.local_participant().projects.iter().peekable(); let mut projects = room.local_participant().projects.iter().peekable();
@ -830,6 +837,7 @@ impl CollabPanel {
let participant = &room.remote_participants()[&user_id]; let participant = &room.remote_participants()[&user_id];
self.entries.push(ListEntry::CallParticipant { self.entries.push(ListEntry::CallParticipant {
user: participant.user.clone(), user: participant.user.clone(),
peer_id: Some(participant.peer_id),
is_pending: false, is_pending: false,
}); });
let mut projects = participant.projects.iter().peekable(); let mut projects = participant.projects.iter().peekable();
@ -871,6 +879,7 @@ impl CollabPanel {
self.entries self.entries
.extend(matches.iter().map(|mat| ListEntry::CallParticipant { .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
user: room.pending_participants()[mat.candidate_id].clone(), user: room.pending_participants()[mat.candidate_id].clone(),
peer_id: None,
is_pending: true, is_pending: true,
})); }));
} }
@ -1174,46 +1183,97 @@ impl CollabPanel {
fn render_call_participant( fn render_call_participant(
user: &User, user: &User,
peer_id: Option<PeerId>,
user_store: ModelHandle<UserStore>,
is_pending: bool, is_pending: bool,
is_selected: bool, is_selected: bool,
theme: &theme::CollabPanel, theme: &theme::Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
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() Flex::row()
.with_children(user.avatar.clone().map(|avatar| { .with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar) Image::from_data(avatar)
.with_style(theme.contact_avatar) .with_style(collab_theme.contact_avatar)
.aligned() .aligned()
.left() .left()
})) }))
.with_child( .with_child(
Label::new( Label::new(
user.github_login.clone(), user.github_login.clone(),
theme.contact_username.text.clone(), collab_theme.contact_username.text.clone(),
) )
.contained() .contained()
.with_style(theme.contact_username.container) .with_style(collab_theme.contact_username.container)
.aligned() .aligned()
.left() .left()
.flex(1., true), .flex(1., true),
) )
.with_children(if is_pending { .with_children(if is_pending {
Some( Some(
Label::new("Calling", theme.calling_indicator.text.clone()) Label::new("Calling", collab_theme.calling_indicator.text.clone())
.contained() .contained()
.with_style(theme.calling_indicator.container) .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(), .aligned(),
) )
} else { } else {
None None
}) })
.constrained() .constrained()
.with_height(theme.row_height) .with_height(collab_theme.row_height)
.contained() .contained()
.with_style( .with_style(style)
*theme });
.contact_row
.in_state(is_selected) if is_current_user || is_pending || peer_id.is_none() {
.style_for(&mut Default::default()), 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));
}
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<CallParticipantTooltip>(
user.id as usize,
tooltip,
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
) )
.into_any() .into_any()
} }
@ -1225,48 +1285,58 @@ impl CollabPanel {
is_current: bool, is_current: bool,
is_last: bool, is_last: bool,
is_selected: bool, is_selected: bool,
theme: &theme::CollabPanel, theme: &theme::Theme,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
enum JoinProject {} enum JoinProject {}
enum JoinProjectTooltip {}
let host_avatar_width = theme let collab_theme = &theme.collab_panel;
let host_avatar_width = collab_theme
.contact_avatar .contact_avatar
.width .width
.or(theme.contact_avatar.height) .or(collab_theme.contact_avatar.height)
.unwrap_or(0.); .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() { let project_name = if worktree_root_names.is_empty() {
"untitled".to_string() "untitled".to_string()
} else { } else {
worktree_root_names.join(", ") worktree_root_names.join(", ")
}; };
let content =
MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| { 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 tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme let row = if is_current {
collab_theme
.project_row
.in_state(true)
.style_for(&mut Default::default())
} else {
collab_theme
.project_row .project_row
.in_state(is_selected) .in_state(is_selected)
.style_for(mouse_state); .style_for(mouse_state)
};
Flex::row() Flex::row()
.with_child(render_tree_branch( .with_child(render_tree_branch(
tree_branch, tree_branch,
&row.name.text, &row.name.text,
is_last, is_last,
vec2f(host_avatar_width, theme.row_height), vec2f(host_avatar_width, collab_theme.row_height),
cx.font_cache(), cx.font_cache(),
)) ))
.with_child( .with_child(
Svg::new("icons/file_icons/folder.svg") Svg::new("icons/file_icons/folder.svg")
.with_color(theme.channel_hash.color) .with_color(collab_theme.channel_hash.color)
.constrained() .constrained()
.with_width(theme.channel_hash.width) .with_width(collab_theme.channel_hash.width)
.aligned() .aligned()
.left(), .left(),
) )
.with_child( .with_child(
Label::new(project_name, row.name.text.clone()) Label::new(project_name.clone(), row.name.text.clone())
.aligned() .aligned()
.left() .left()
.contained() .contained()
@ -1274,24 +1344,31 @@ impl CollabPanel {
.flex(1., false), .flex(1., false),
) )
.constrained() .constrained()
.with_height(theme.row_height) .with_height(collab_theme.row_height)
.contained() .contained()
.with_style(row.container) .with_style(row.container)
}) });
.with_cursor_style(if !is_current {
CursorStyle::PointingHand if is_current {
} else { return content.into_any();
CursorStyle::Arrow }
})
content
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if !is_current {
if let Some(workspace) = this.workspace.upgrade(cx) { if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone(); let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx) workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
}
}) })
.with_tooltip::<JoinProjectTooltip>(
project_id as usize,
format!("Open {}", project_name),
None,
theme.tooltip.clone(),
cx,
)
.into_any() .into_any()
} }
@ -2755,7 +2832,7 @@ impl CollabPanel {
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) { fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
ChannelView::deploy(action.channel_id, workspace, cx); ChannelView::open(action.channel_id, workspace, cx).detach();
} }
} }

View file

@ -215,7 +215,13 @@ impl CollabTitlebarItem {
let git_style = theme.titlebar.git_menu_button.clone(); let git_style = theme.titlebar.git_menu_button.clone();
let item_spacing = theme.titlebar.item_spacing; 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() Stack::new()
.with_child( .with_child(
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| { MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@ -283,6 +289,71 @@ impl CollabTitlebarItem {
ret.into_any() 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>) { fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active { let project = if active {
Some(self.project.clone()) Some(self.project.clone())
@ -877,7 +948,7 @@ impl CollabTitlebarItem {
fn render_face_pile( fn render_face_pile(
&self, &self,
user: &User, user: &User,
replica_id: Option<ReplicaId>, _replica_id: Option<ReplicaId>,
peer_id: PeerId, peer_id: PeerId,
location: Option<ParticipantLocation>, location: Option<ParticipantLocation>,
muted: bool, muted: bool,
@ -886,23 +957,20 @@ impl CollabTitlebarItem {
theme: &Theme, theme: &Theme,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let user_id = user.id;
let project_id = workspace.read(cx).project().read(cx).remote_id(); let project_id = workspace.read(cx).project().read(cx).remote_id();
let room = ActiveCall::global(cx).read(cx).room(); let room = ActiveCall::global(cx).read(cx).room().cloned();
let is_being_followed = workspace.read(cx).is_being_followed(peer_id); let self_peer_id = workspace.read(cx).client().peer_id();
let followed_by_self = room let self_following = workspace.read(cx).is_being_followed(peer_id);
.and_then(|room| { let self_following_initialized = self_following
Some( && room.as_ref().map_or(false, |room| match project_id {
is_being_followed None => true,
&& room Some(project_id) => room
.read(cx) .read(cx)
.followers_for(peer_id, project_id?) .followers_for(peer_id, project_id)
.iter() .iter()
.any(|&follower| { .any(|&follower| Some(follower) == self_peer_id),
Some(follower) == workspace.read(cx).client().peer_id() });
}),
)
})
.unwrap_or(false);
let leader_style = theme.titlebar.leader_avatar; let leader_style = theme.titlebar.leader_avatar;
let follower_style = theme.titlebar.follower_avatar; let follower_style = theme.titlebar.follower_avatar;
@ -921,15 +989,30 @@ impl CollabTitlebarItem {
.background_color .background_color
.unwrap_or_default(); .unwrap_or_default();
if let Some(replica_id) = replica_id { let participant_index = self
if followed_by_self { .user_store
let selection = theme.editor.replica_selection_style(replica_id).selection; .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 = Color::blend(selection, background_color);
background_color.a = 255; background_color.a = 255;
} }
} }
let mut content = Stack::new() enum TitlebarParticipant {}
let content = MouseEventHandler::new::<TitlebarParticipant, _>(
peer_id.as_u64() as usize,
cx,
move |_, cx| {
Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| { .with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap) let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
.with_child(Self::render_face( .with_child(Self::render_face(
@ -943,29 +1026,14 @@ impl CollabTitlebarItem {
let project_id = project_id?; let project_id = project_id?;
let room = room?.read(cx); let room = room?.read(cx);
let followers = room.followers_for(peer_id, project_id); let followers = room.followers_for(peer_id, project_id);
Some(followers.into_iter().filter_map(|&follower| {
Some(followers.into_iter().flat_map(|&follower| { if Some(follower) == self_peer_id {
let remote_participant = return None;
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
} }
})?; let participant =
room.remote_participant_for_peer_id(follower)?;
Some(Self::render_face( Some(Self::render_face(
avatar.clone(), participant.user.avatar.clone()?,
follower_style, follower_style,
background_color, background_color,
None, None,
@ -974,15 +1042,30 @@ impl CollabTitlebarItem {
})() })()
.into_iter() .into_iter()
.flatten(), .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 let mut container = face_pile
.contained() .contained()
.with_style(theme.titlebar.leader_selection); .with_style(theme.titlebar.leader_selection);
if let Some(replica_id) = replica_id { if let Some(participant_index) = participant_index {
if followed_by_self { if self_following_initialized {
let color = theme.editor.replica_selection_style(replica_id).selection; let color = theme
.editor
.selection_style_for_room_participant(participant_index.0)
.selection;
container = container.with_background_color(color); container = container.with_background_color(color);
} }
} }
@ -990,8 +1073,11 @@ impl CollabTitlebarItem {
container container
})) }))
.with_children((|| { .with_children((|| {
let replica_id = replica_id?; let participant_index = participant_index?;
let color = theme.editor.replica_selection_style(replica_id).cursor; let color = theme
.editor
.selection_style_for_room_participant(participant_index.0)
.cursor;
Some( Some(
AvatarRibbon::new(color) AvatarRibbon::new(color)
.constrained() .constrained()
@ -1001,67 +1087,33 @@ impl CollabTitlebarItem {
.bottom(), .bottom(),
) )
})()) })())
.into_any(); },
);
if let Some(location) = location { if Some(peer_id) == self_peer_id {
if let Some(replica_id) = replica_id { return content.into_any();
enum ToggleFollow {} }
content = MouseEventHandler::new::<ToggleFollow, _>( content
replica_id.into(),
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, item, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = item.workspace.upgrade(cx) { let Some(workspace) = this.workspace.upgrade(cx) else {
if let Some(task) = workspace return;
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) };
if let Some(task) =
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
{ {
task.detach_and_log_err(cx); task.detach_and_log_err(cx);
} }
}
}) })
.with_tooltip::<ToggleFollow>( .with_tooltip::<TitlebarParticipant>(
peer_id.as_u64() as usize, peer_id.as_u64() as usize,
if is_being_followed { format!("Follow {}", user.github_login),
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)), Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(), theme.tooltip.clone(),
cx, cx,
) )
.into_any(); .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();
}
}
content
} }
fn location_style( fn location_style(

View file

@ -7,7 +7,7 @@ mod face_pile;
mod incoming_call_notification; mod incoming_call_notification;
mod notifications; mod notifications;
mod panel_settings; mod panel_settings;
mod project_shared_notification; pub mod project_shared_notification;
mod sharing_status_indicator; mod sharing_status_indicator;
use call::{report_call_event_for_room, ActiveCall, Room}; use call::{report_call_event_for_room, ActiveCall, Room};

View file

@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.push(window); .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) { if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows { for window in windows {
window.remove(cx); window.remove(cx);
@ -82,7 +84,6 @@ impl ProjectSharedNotification {
} }
fn join(&mut self, cx: &mut ViewContext<Self>) { fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() { if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -90,7 +91,15 @@ impl ProjectSharedNotification {
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { 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> { fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

View file

@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::{ClickhouseEvent, TelemetrySettings}; use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::{Global, ReplicaId}; use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
@ -79,6 +79,7 @@ pub use multi_buffer::{
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use rpc::proto::PeerId;
use scroll::{ use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
}; };
@ -581,11 +582,11 @@ pub struct Editor {
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>, get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
override_text_style: Option<Box<OverrideTextStyle>>, override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<ModelHandle<Project>>, project: Option<ModelHandle<Project>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
focused: bool, focused: bool,
blink_manager: ModelHandle<BlinkManager>, blink_manager: ModelHandle<BlinkManager>,
pub show_local_selections: bool, pub show_local_selections: bool,
mode: EditorMode, mode: EditorMode,
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
show_gutter: bool, show_gutter: bool,
show_wrap_guides: Option<bool>, show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>, placeholder_text: Option<Arc<str>>,
@ -609,7 +610,7 @@ pub struct Editor {
keymap_context_layers: BTreeMap<TypeId, KeymapContext>, keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool, input_enabled: bool,
read_only: bool, read_only: bool,
leader_replica_id: Option<u16>, leader_peer_id: Option<PeerId>,
remote_id: Option<ViewId>, remote_id: Option<ViewId>,
hover_state: HoverState, hover_state: HoverState,
gutter_hovered: bool, gutter_hovered: bool,
@ -631,6 +632,15 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll, 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)] #[derive(Clone, Debug)]
struct SelectionHistoryEntry { struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>, selections: Arc<[Selection<Anchor>]>,
@ -1047,7 +1057,8 @@ impl CompletionsMenu {
item_ix: Some(item_ix), item_ix: Some(item_ix),
}, },
cx, cx,
); )
.map(|task| task.detach());
}) })
.into_any(), .into_any(),
); );
@ -1539,12 +1550,12 @@ impl Editor {
active_diagnostics: None, active_diagnostics: None,
soft_wrap_mode_override, soft_wrap_mode_override,
get_field_editor_theme, get_field_editor_theme,
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project, project,
focused: false, focused: false,
blink_manager: blink_manager.clone(), blink_manager: blink_manager.clone(),
show_local_selections: true, show_local_selections: true,
mode, mode,
replica_id_mapping: None,
show_gutter: mode == EditorMode::Full, show_gutter: mode == EditorMode::Full,
show_wrap_guides: None, show_wrap_guides: None,
placeholder_text: None, placeholder_text: None,
@ -1571,7 +1582,7 @@ impl Editor {
keymap_context_layers: Default::default(), keymap_context_layers: Default::default(),
input_enabled: true, input_enabled: true,
read_only: false, read_only: false,
leader_replica_id: None, leader_peer_id: None,
remote_id: None, remote_id: None,
hover_state: Default::default(), hover_state: Default::default(),
link_go_to_definition_state: Default::default(), link_go_to_definition_state: Default::default(),
@ -1658,8 +1669,8 @@ impl Editor {
self.buffer.read(cx).replica_id() self.buffer.read(cx).replica_id()
} }
pub fn leader_replica_id(&self) -> Option<ReplicaId> { pub fn leader_peer_id(&self) -> Option<PeerId> {
self.leader_replica_id self.leader_peer_id
} }
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> { pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
@ -1723,6 +1734,14 @@ impl Editor {
self.mode 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( pub fn set_placeholder_text(
&mut self, &mut self,
placeholder_text: impl Into<Arc<str>>, placeholder_text: impl Into<Arc<str>>,
@ -1799,26 +1818,13 @@ impl Editor {
cx.notify(); 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( fn selections_did_change(
&mut self, &mut self,
local: bool, local: bool,
old_cursor_position: &Anchor, old_cursor_position: &Anchor,
cx: &mut ViewContext<Self>, 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| { self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections( buffer.set_active_selections(
&self.selections.disjoint_anchors(), &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( fn inlay_hint_settings(
location: Anchor, location: Anchor,
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
@ -8668,6 +8695,34 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
} }
impl EditorSnapshot { 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>> { pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position) self.display_snapshot.buffer_snapshot.language_at(position)
} }
@ -8781,7 +8836,7 @@ impl View for Editor {
self.focused = true; self.focused = true;
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx); buffer.finalize_last_transaction(cx);
if self.leader_replica_id.is_none() { if self.leader_peer_id.is_none() {
buffer.set_active_selections( buffer.set_active_selections(
&self.selections.disjoint_anchors(), &self.selections.disjoint_anchors(),
self.selections.line_mode, self.selections.line_mode,

View file

@ -17,7 +17,6 @@ use crate::{
}, },
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
}; };
use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use git::diff::DiffHunkStatus; use git::diff::DiffHunkStatus;
use gpui::{ use gpui::{
@ -55,6 +54,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use text::Point; use text::Point;
use theme::SelectionStyle;
use workspace::item::Item; use workspace::item::Item;
enum FoldMarkers {} enum FoldMarkers {}
@ -868,14 +868,7 @@ impl EditorElement {
let corner_radius = 0.15 * layout.position_map.line_height; let corner_radius = 0.15 * layout.position_map.line_height;
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new(); let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
for (replica_id, selections) in &layout.selections { for (selection_style, 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 in selections { for selection in selections {
self.paint_highlighted_range( self.paint_highlighted_range(
selection.range.clone(), selection.range.clone(),
@ -2193,7 +2186,7 @@ impl Element<Editor> for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) .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 active_rows = BTreeMap::new();
let mut fold_ranges = Vec::new(); let mut fold_ranges = Vec::new();
let is_singleton = editor.is_singleton(cx); 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; let mut newest_selection_head = None;
if editor.show_local_selections { if editor.show_local_selections {
@ -2282,19 +2246,58 @@ impl Element<Editor> for EditorElement {
layouts.push(layout); layouts.push(layout);
} }
// Render the local selections in the leader's color when following. selections.push((style.selection, layouts));
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)
} else {
replica_id
} }
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 {
style.absent_selection
}; };
selections.push((Some(local_replica_id), layouts)); // Don't re-render the leader's selections, since the local selections
// match theirs.
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
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; let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@ -2686,7 +2689,7 @@ pub struct LayoutState {
blocks: Vec<BlockLayout>, blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
fold_ranges: Vec<(BufferRow, 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>, scrollbar_row_range: Range<f32>,
show_scrollbars: bool, show_scrollbars: bool,
is_singleton: bool, is_singleton: bool,

View file

@ -17,7 +17,7 @@ use language::{
SelectionGoal, SelectionGoal,
}; };
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; 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 smallvec::SmallVec;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -156,13 +156,9 @@ impl FollowableItem for Editor {
})) }))
} }
fn set_leader_replica_id( fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
&mut self, self.leader_peer_id = leader_peer_id;
leader_replica_id: Option<u16>, if self.leader_peer_id.is_some() {
cx: &mut ViewContext<Self>,
) {
self.leader_replica_id = leader_replica_id;
if self.leader_replica_id.is_some() {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx); buffer.remove_active_selections(cx);
}); });
@ -309,6 +305,10 @@ impl FollowableItem for Editor {
_ => false, _ => false,
} }
} }
fn is_project_item(&self, _cx: &AppContext) -> bool {
true
}
} }
async fn update_editor_from_message( async fn update_editor_from_message(

View file

@ -33,7 +33,7 @@ lazy_static.workspace = true
postage.workspace = true postage.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.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" } tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2" urlencoding = "2.1.2"

View file

@ -9,8 +9,6 @@ path = "src/fs.rs"
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rope = { path = "../rope" } rope = { path = "../rope" }
text = { path = "../text" } text = { path = "../text" }
util = { path = "../util" } util = { path = "../util" }
@ -34,8 +32,10 @@ log.workspace = true
libc = "0.2" libc = "0.2"
time.workspace = true time.workspace = true
gpui = { path = "../gpui", optional = true}
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
[features] [features]
test-support = [] test-support = ["gpui/test-support"]

View file

@ -93,33 +93,6 @@ pub struct Metadata {
pub is_dir: bool, 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; pub struct RealFs;
#[async_trait::async_trait] #[async_trait::async_trait]

View file

@ -11,7 +11,7 @@ path = "src/gpui.rs"
doctest = false doctest = false
[features] [features]
test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"] test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }

View file

@ -103,6 +103,7 @@ pub struct Platform {
current_clipboard_item: Mutex<Option<ClipboardItem>>, current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>, cursor: Mutex<CursorStyle>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_screen: Screen,
} }
impl Platform { impl Platform {
@ -113,6 +114,7 @@ impl Platform {
current_clipboard_item: Default::default(), current_clipboard_item: Default::default(),
cursor: Mutex::new(CursorStyle::Arrow), cursor: Mutex::new(CursorStyle::Arrow),
active_window: Default::default(), active_window: Default::default(),
active_screen: Screen::new(),
} }
} }
} }
@ -136,12 +138,16 @@ impl super::Platform for Platform {
fn quit(&self) {} fn quit(&self) {}
fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> { 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 None
} }
}
fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> { fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
Default::default() vec![Rc::new(self.active_screen.clone())]
} }
fn open_window( fn open_window(
@ -158,6 +164,7 @@ impl super::Platform for Platform {
WindowBounds::Fixed(rect) => rect.size(), WindowBounds::Fixed(rect) => rect.size(),
}, },
self.active_window.clone(), self.active_window.clone(),
Rc::new(self.active_screen.clone()),
)) ))
} }
@ -170,6 +177,7 @@ impl super::Platform for Platform {
handle, handle,
vec2f(24., 24.), vec2f(24., 24.),
self.active_window.clone(), self.active_window.clone(),
Rc::new(self.active_screen.clone()),
)) ))
} }
@ -238,8 +246,18 @@ impl super::Platform for Platform {
fn restart(&self) {} fn restart(&self) {}
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Screen; pub struct Screen {
uuid: uuid::Uuid,
}
impl Screen {
fn new() -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
}
}
}
impl super::Screen for Screen { impl super::Screen for Screen {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
@ -255,7 +273,7 @@ impl super::Screen for Screen {
} }
fn display_uuid(&self) -> Option<uuid::Uuid> { 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) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>, pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
} }
impl Window { impl Window {
@ -282,6 +301,7 @@ impl Window {
handle: AnyWindowHandle, handle: AnyWindowHandle,
size: Vector2F, size: Vector2F,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
) -> Self { ) -> Self {
Self { Self {
handle, handle,
@ -299,6 +319,7 @@ impl Window {
edited: false, edited: false,
pending_prompts: Default::default(), pending_prompts: Default::default(),
active_window, active_window,
screen,
} }
} }
@ -329,7 +350,7 @@ impl super::Window for Window {
} }
fn screen(&self) -> Rc<dyn crate::platform::Screen> { fn screen(&self) -> Rc<dyn crate::platform::Screen> {
Rc::new(Screen) self.screen.clone()
} }
fn mouse_position(&self) -> Vector2F { fn mouse_position(&self) -> Vector2F {

View file

@ -11,7 +11,7 @@ mod project_tests;
mod worktree_tests; mod worktree_tests;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{proto, Client, TypedEnvelope, UserId, UserStore}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use copilot::Copilot; 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)] #[derive(Clone, Debug, PartialEq)]
pub enum Event { pub enum Event {
LanguageServerAdded(LanguageServerId), LanguageServerAdded(LanguageServerId),
@ -982,6 +975,10 @@ impl Project {
&self.collaborators &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 /// Collect all worktrees, including ones that don't appear in the project panel
pub fn worktrees<'a>( pub fn worktrees<'a>(
&'a self, &'a self,
@ -2231,10 +2228,12 @@ impl Project {
.get_mut(&buffer.remote_id()) .get_mut(&buffer.remote_id())
.and_then(|m| m.get_mut(&language_server.server_id()))?; .and_then(|m| m.get_mut(&language_server.server_id()))?;
let previous_snapshot = buffer_snapshots.last()?; let previous_snapshot = buffer_snapshots.last()?;
let next_version = previous_snapshot.version + 1;
let content_changes = buffer let build_incremental_change = || {
.edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version()) buffer
.edits_since::<(PointUtf16, usize)>(
previous_snapshot.snapshot.version(),
)
.map(|edit| { .map(|edit| {
let edit_start = edit.new.start.0; let edit_start = edit.new.start.0;
let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
@ -2250,7 +2249,41 @@ impl Project {
text: new_text, text: new_text,
} }
}) })
.collect(); .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: next_snapshot.text(),
}]
}
Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(),
_ => {
#[cfg(any(test, feature = "test-support"))]
{
build_incremental_change()
}
#[cfg(not(any(test, feature = "test-support")))]
{
continue;
}
}
};
let next_version = previous_snapshot.version + 1;
buffer_snapshots.push(LspBufferSnapshot { buffer_snapshots.push(LspBufferSnapshot {
version: next_version, version: next_version,
@ -4928,7 +4961,15 @@ impl Project {
if abs_path.ends_with("/") { if abs_path.ends_with("/") {
fs.create_dir(&abs_path).await?; fs.create_dir(&abs_path).await?;
} else { } else {
fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) 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?; .await?;
} }
} }
@ -4945,7 +4986,12 @@ impl Project {
fs.rename( fs.rename(
&source_abs_path, &source_abs_path,
&target_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?; .await?;
} }
@ -4955,7 +5001,13 @@ impl Project {
.uri .uri
.to_file_path() .to_file_path()
.map_err(|_| anyhow!("can't convert URI to 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("/") { if abs_path.ends_with("/") {
fs.remove_dir(&abs_path, options).await?; fs.remove_dir(&abs_path, options).await?;
} else { } 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 { impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
fn from((worktree_id, path): (WorktreeId, P)) -> Self { fn from((worktree_id, path): (WorktreeId, P)) -> Self {
Self { Self {

View file

@ -84,6 +84,7 @@ impl Project {
terminal_settings::ActivateScript::Default => "activate", terminal_settings::ActivateScript::Default => "activate",
terminal_settings::ActivateScript::Csh => "activate.csh", terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish", terminal_settings::ActivateScript::Fish => "activate.fish",
terminal_settings::ActivateScript::Nushell => "activate.nu",
}; };
for virtual_environment_name in settings.directories { for virtual_environment_name in settings.directories {

View file

@ -23,154 +23,152 @@ message Envelope {
CreateRoomResponse create_room_response = 10; CreateRoomResponse create_room_response = 10;
JoinRoom join_room = 11; JoinRoom join_room = 11;
JoinRoomResponse join_room_response = 12; JoinRoomResponse join_room_response = 12;
RejoinRoom rejoin_room = 108; RejoinRoom rejoin_room = 13;
RejoinRoomResponse rejoin_room_response = 109; RejoinRoomResponse rejoin_room_response = 14;
LeaveRoom leave_room = 13; LeaveRoom leave_room = 15;
Call call = 14; Call call = 16;
IncomingCall incoming_call = 15; IncomingCall incoming_call = 17;
CallCanceled call_canceled = 16; CallCanceled call_canceled = 18;
CancelCall cancel_call = 17; CancelCall cancel_call = 19;
DeclineCall decline_call = 18; DeclineCall decline_call = 20;
UpdateParticipantLocation update_participant_location = 19; UpdateParticipantLocation update_participant_location = 21;
RoomUpdated room_updated = 20; RoomUpdated room_updated = 22;
ShareProject share_project = 21; ShareProject share_project = 23;
ShareProjectResponse share_project_response = 22; ShareProjectResponse share_project_response = 24;
UnshareProject unshare_project = 23; UnshareProject unshare_project = 25;
JoinProject join_project = 24; JoinProject join_project = 26;
JoinProjectResponse join_project_response = 25; JoinProjectResponse join_project_response = 27;
LeaveProject leave_project = 26; LeaveProject leave_project = 28;
AddProjectCollaborator add_project_collaborator = 27; AddProjectCollaborator add_project_collaborator = 29;
UpdateProjectCollaborator update_project_collaborator = 110; UpdateProjectCollaborator update_project_collaborator = 30;
RemoveProjectCollaborator remove_project_collaborator = 28; RemoveProjectCollaborator remove_project_collaborator = 31;
GetDefinition get_definition = 29; GetDefinition get_definition = 32;
GetDefinitionResponse get_definition_response = 30; GetDefinitionResponse get_definition_response = 33;
GetTypeDefinition get_type_definition = 31; GetTypeDefinition get_type_definition = 34;
GetTypeDefinitionResponse get_type_definition_response = 32; GetTypeDefinitionResponse get_type_definition_response = 35;
GetReferences get_references = 33; GetReferences get_references = 36;
GetReferencesResponse get_references_response = 34; GetReferencesResponse get_references_response = 37;
GetDocumentHighlights get_document_highlights = 35; GetDocumentHighlights get_document_highlights = 38;
GetDocumentHighlightsResponse get_document_highlights_response = 36; GetDocumentHighlightsResponse get_document_highlights_response = 39;
GetProjectSymbols get_project_symbols = 37; GetProjectSymbols get_project_symbols = 40;
GetProjectSymbolsResponse get_project_symbols_response = 38; GetProjectSymbolsResponse get_project_symbols_response = 41;
OpenBufferForSymbol open_buffer_for_symbol = 39; OpenBufferForSymbol open_buffer_for_symbol = 42;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40; OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
UpdateProject update_project = 41; UpdateProject update_project = 44;
UpdateWorktree update_worktree = 43; UpdateWorktree update_worktree = 45;
CreateProjectEntry create_project_entry = 45; CreateProjectEntry create_project_entry = 46;
RenameProjectEntry rename_project_entry = 46; RenameProjectEntry rename_project_entry = 47;
CopyProjectEntry copy_project_entry = 47; CopyProjectEntry copy_project_entry = 48;
DeleteProjectEntry delete_project_entry = 48; DeleteProjectEntry delete_project_entry = 49;
ProjectEntryResponse project_entry_response = 49; ProjectEntryResponse project_entry_response = 50;
ExpandProjectEntry expand_project_entry = 114; ExpandProjectEntry expand_project_entry = 51;
ExpandProjectEntryResponse expand_project_entry_response = 115; ExpandProjectEntryResponse expand_project_entry_response = 52;
UpdateDiagnosticSummary update_diagnostic_summary = 50; UpdateDiagnosticSummary update_diagnostic_summary = 53;
StartLanguageServer start_language_server = 51; StartLanguageServer start_language_server = 54;
UpdateLanguageServer update_language_server = 52; UpdateLanguageServer update_language_server = 55;
OpenBufferById open_buffer_by_id = 53; OpenBufferById open_buffer_by_id = 56;
OpenBufferByPath open_buffer_by_path = 54; OpenBufferByPath open_buffer_by_path = 57;
OpenBufferResponse open_buffer_response = 55; OpenBufferResponse open_buffer_response = 58;
CreateBufferForPeer create_buffer_for_peer = 56; CreateBufferForPeer create_buffer_for_peer = 59;
UpdateBuffer update_buffer = 57; UpdateBuffer update_buffer = 60;
UpdateBufferFile update_buffer_file = 58; UpdateBufferFile update_buffer_file = 61;
SaveBuffer save_buffer = 59; SaveBuffer save_buffer = 62;
BufferSaved buffer_saved = 60; BufferSaved buffer_saved = 63;
BufferReloaded buffer_reloaded = 61; BufferReloaded buffer_reloaded = 64;
ReloadBuffers reload_buffers = 62; ReloadBuffers reload_buffers = 65;
ReloadBuffersResponse reload_buffers_response = 63; ReloadBuffersResponse reload_buffers_response = 66;
SynchronizeBuffers synchronize_buffers = 200; SynchronizeBuffers synchronize_buffers = 67;
SynchronizeBuffersResponse synchronize_buffers_response = 201; SynchronizeBuffersResponse synchronize_buffers_response = 68;
FormatBuffers format_buffers = 64; FormatBuffers format_buffers = 69;
FormatBuffersResponse format_buffers_response = 65; FormatBuffersResponse format_buffers_response = 70;
GetCompletions get_completions = 66; GetCompletions get_completions = 71;
GetCompletionsResponse get_completions_response = 67; GetCompletionsResponse get_completions_response = 72;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68; ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69; ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
GetCodeActions get_code_actions = 70; GetCodeActions get_code_actions = 75;
GetCodeActionsResponse get_code_actions_response = 71; GetCodeActionsResponse get_code_actions_response = 76;
GetHover get_hover = 72; GetHover get_hover = 77;
GetHoverResponse get_hover_response = 73; GetHoverResponse get_hover_response = 78;
ApplyCodeAction apply_code_action = 74; ApplyCodeAction apply_code_action = 79;
ApplyCodeActionResponse apply_code_action_response = 75; ApplyCodeActionResponse apply_code_action_response = 80;
PrepareRename prepare_rename = 76; PrepareRename prepare_rename = 81;
PrepareRenameResponse prepare_rename_response = 77; PrepareRenameResponse prepare_rename_response = 82;
PerformRename perform_rename = 78; PerformRename perform_rename = 83;
PerformRenameResponse perform_rename_response = 79; PerformRenameResponse perform_rename_response = 84;
SearchProject search_project = 80; SearchProject search_project = 85;
SearchProjectResponse search_project_response = 81; SearchProjectResponse search_project_response = 86;
UpdateContacts update_contacts = 92; UpdateContacts update_contacts = 87;
UpdateInviteInfo update_invite_info = 93; UpdateInviteInfo update_invite_info = 88;
ShowContacts show_contacts = 94; ShowContacts show_contacts = 89;
GetUsers get_users = 95; GetUsers get_users = 90;
FuzzySearchUsers fuzzy_search_users = 96; FuzzySearchUsers fuzzy_search_users = 91;
UsersResponse users_response = 97; UsersResponse users_response = 92;
RequestContact request_contact = 98; RequestContact request_contact = 93;
RespondToContactRequest respond_to_contact_request = 99; RespondToContactRequest respond_to_contact_request = 94;
RemoveContact remove_contact = 100; RemoveContact remove_contact = 95;
Follow follow = 101; Follow follow = 96;
FollowResponse follow_response = 102; FollowResponse follow_response = 97;
UpdateFollowers update_followers = 103; UpdateFollowers update_followers = 98;
Unfollow unfollow = 104; Unfollow unfollow = 99;
GetPrivateUserInfo get_private_user_info = 105; GetPrivateUserInfo get_private_user_info = 100;
GetPrivateUserInfoResponse get_private_user_info_response = 106; GetPrivateUserInfoResponse get_private_user_info_response = 101;
UpdateDiffBase update_diff_base = 107; UpdateDiffBase update_diff_base = 102;
OnTypeFormatting on_type_formatting = 111; OnTypeFormatting on_type_formatting = 103;
OnTypeFormattingResponse on_type_formatting_response = 112; OnTypeFormattingResponse on_type_formatting_response = 104;
UpdateWorktreeSettings update_worktree_settings = 113; UpdateWorktreeSettings update_worktree_settings = 105;
InlayHints inlay_hints = 116; InlayHints inlay_hints = 106;
InlayHintsResponse inlay_hints_response = 117; InlayHintsResponse inlay_hints_response = 107;
ResolveInlayHint resolve_inlay_hint = 137; ResolveInlayHint resolve_inlay_hint = 108;
ResolveInlayHintResponse resolve_inlay_hint_response = 138; ResolveInlayHintResponse resolve_inlay_hint_response = 109;
RefreshInlayHints refresh_inlay_hints = 118; RefreshInlayHints refresh_inlay_hints = 110;
CreateChannel create_channel = 119; CreateChannel create_channel = 111;
CreateChannelResponse create_channel_response = 120; CreateChannelResponse create_channel_response = 112;
InviteChannelMember invite_channel_member = 121; InviteChannelMember invite_channel_member = 113;
RemoveChannelMember remove_channel_member = 122; RemoveChannelMember remove_channel_member = 114;
RespondToChannelInvite respond_to_channel_invite = 123; RespondToChannelInvite respond_to_channel_invite = 115;
UpdateChannels update_channels = 124; UpdateChannels update_channels = 116;
JoinChannel join_channel = 125; JoinChannel join_channel = 117;
DeleteChannel delete_channel = 126; DeleteChannel delete_channel = 118;
GetChannelMembers get_channel_members = 127; GetChannelMembers get_channel_members = 119;
GetChannelMembersResponse get_channel_members_response = 128; GetChannelMembersResponse get_channel_members_response = 120;
SetChannelMemberAdmin set_channel_member_admin = 129; SetChannelMemberAdmin set_channel_member_admin = 121;
RenameChannel rename_channel = 130; RenameChannel rename_channel = 122;
RenameChannelResponse rename_channel_response = 154; RenameChannelResponse rename_channel_response = 123;
JoinChannelBuffer join_channel_buffer = 131; JoinChannelBuffer join_channel_buffer = 124;
JoinChannelBufferResponse join_channel_buffer_response = 132; JoinChannelBufferResponse join_channel_buffer_response = 125;
UpdateChannelBuffer update_channel_buffer = 133; UpdateChannelBuffer update_channel_buffer = 126;
LeaveChannelBuffer leave_channel_buffer = 134; LeaveChannelBuffer leave_channel_buffer = 127;
AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; RejoinChannelBuffers rejoin_channel_buffers = 129;
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139; RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
RejoinChannelBuffers rejoin_channel_buffers = 140;
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
JoinChannelChat join_channel_chat = 142; JoinChannelChat join_channel_chat = 131;
JoinChannelChatResponse join_channel_chat_response = 143; JoinChannelChatResponse join_channel_chat_response = 132;
LeaveChannelChat leave_channel_chat = 144; LeaveChannelChat leave_channel_chat = 133;
SendChannelMessage send_channel_message = 145; SendChannelMessage send_channel_message = 134;
SendChannelMessageResponse send_channel_message_response = 146; SendChannelMessageResponse send_channel_message_response = 135;
ChannelMessageSent channel_message_sent = 147; ChannelMessageSent channel_message_sent = 136;
GetChannelMessages get_channel_messages = 148; GetChannelMessages get_channel_messages = 137;
GetChannelMessagesResponse get_channel_messages_response = 149; GetChannelMessagesResponse get_channel_messages_response = 138;
RemoveChannelMessage remove_channel_message = 150; RemoveChannelMessage remove_channel_message = 139;
LinkChannel link_channel = 151; LinkChannel link_channel = 140;
UnlinkChannel unlink_channel = 152; UnlinkChannel unlink_channel = 141;
MoveChannel move_channel = 153; // Current max: 154 MoveChannel move_channel = 142;
} }
} }
@ -258,6 +256,7 @@ message Participant {
PeerId peer_id = 2; PeerId peer_id = 2;
repeated ParticipantProject projects = 3; repeated ParticipantProject projects = 3;
ParticipantLocation location = 4; ParticipantLocation location = 4;
uint32 participant_index = 5;
} }
message PendingParticipant { message PendingParticipant {
@ -440,20 +439,9 @@ message RemoveProjectCollaborator {
PeerId peer_id = 2; PeerId peer_id = 2;
} }
message AddChannelBufferCollaborator { message UpdateChannelBufferCollaborators {
uint64 channel_id = 1; uint64 channel_id = 1;
Collaborator collaborator = 2; repeated Collaborator collaborators = 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;
} }
message GetDefinition { message GetDefinition {
@ -1213,8 +1201,9 @@ message UpdateDiagnostics {
} }
message Follow { message Follow {
uint64 project_id = 1; uint64 room_id = 1;
PeerId leader_id = 2; optional uint64 project_id = 2;
PeerId leader_id = 3;
} }
message FollowResponse { message FollowResponse {
@ -1223,18 +1212,20 @@ message FollowResponse {
} }
message UpdateFollowers { message UpdateFollowers {
uint64 project_id = 1; uint64 room_id = 1;
repeated PeerId follower_ids = 2; optional uint64 project_id = 2;
repeated PeerId follower_ids = 3;
oneof variant { oneof variant {
UpdateActiveView update_active_view = 3; UpdateActiveView update_active_view = 4;
View create_view = 4; View create_view = 5;
UpdateView update_view = 5; UpdateView update_view = 6;
} }
} }
message Unfollow { message Unfollow {
uint64 project_id = 1; uint64 room_id = 1;
PeerId leader_id = 2; optional uint64 project_id = 2;
PeerId leader_id = 3;
} }
message GetPrivateUserInfo {} message GetPrivateUserInfo {}

View file

@ -270,9 +270,7 @@ messages!(
(JoinChannelBufferResponse, Foreground), (JoinChannelBufferResponse, Foreground),
(LeaveChannelBuffer, Background), (LeaveChannelBuffer, Background),
(UpdateChannelBuffer, Foreground), (UpdateChannelBuffer, Foreground),
(RemoveChannelBufferCollaborator, Foreground), (UpdateChannelBufferCollaborators, Foreground),
(AddChannelBufferCollaborator, Foreground),
(UpdateChannelBufferCollaborator, Foreground),
); );
request_messages!( request_messages!(
@ -364,7 +362,6 @@ entity_messages!(
CreateProjectEntry, CreateProjectEntry,
DeleteProjectEntry, DeleteProjectEntry,
ExpandProjectEntry, ExpandProjectEntry,
Follow,
FormatBuffers, FormatBuffers,
GetCodeActions, GetCodeActions,
GetCompletions, GetCompletions,
@ -392,12 +389,10 @@ entity_messages!(
SearchProject, SearchProject,
StartLanguageServer, StartLanguageServer,
SynchronizeBuffers, SynchronizeBuffers,
Unfollow,
UnshareProject, UnshareProject,
UpdateBuffer, UpdateBuffer,
UpdateBufferFile, UpdateBufferFile,
UpdateDiagnosticSummary, UpdateDiagnosticSummary,
UpdateFollowers,
UpdateLanguageServer, UpdateLanguageServer,
UpdateProject, UpdateProject,
UpdateProjectCollaborator, UpdateProjectCollaborator,
@ -410,10 +405,8 @@ entity_messages!(
channel_id, channel_id,
ChannelMessageSent, ChannelMessageSent,
UpdateChannelBuffer, UpdateChannelBuffer,
RemoveChannelBufferCollaborator,
RemoveChannelMessage, RemoveChannelMessage,
AddChannelBufferCollaborator, UpdateChannelBufferCollaborators
UpdateChannelBufferCollaborator
); );
const KIB: usize = 1024; const KIB: usize = 1024;

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 63; pub const PROTOCOL_VERSION: u32 = 64;

View file

@ -61,8 +61,9 @@ const CODE_CONTEXT_TEMPLATE: &str =
const ENTIRE_FILE_TEMPLATE: &str = const ENTIRE_FILE_TEMPLATE: &str =
"The below snippet is from file '<path>'\n\n```<language>\n<item>\n```"; "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>"; const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
&["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"]; "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
];
pub struct CodeContextRetriever { pub struct CodeContextRetriever {
pub parser: Parser, pub parser: Parser,

View file

@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() {
todo!(); todo!();
} }
} }
#[derive(Clone)]
struct D {
name: String
}
" "
.unindent(); .unindent();
@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() {
.unindent(), .unindent(),
text.find("fn function_2").unwrap(), 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) name: (_) @name)
] @item ] @item
) )
(attribute_item) @collapse
(use_declaration) @collapse
"#, "#,
) )
.unwrap(), .unwrap(),

View file

@ -12,6 +12,8 @@ path = "src/storybook.rs"
anyhow.workspace = true anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] } clap = { version = "4.4", features = ["derive", "string"] }
chrono = "0.4" chrono = "0.4"
fs = { path = "../fs" }
futures.workspace = true
gpui2 = { path = "../gpui2" } gpui2 = { path = "../gpui2" }
itertools = "0.11.0" itertools = "0.11.0"
log.workspace = true log.workspace = true

View file

@ -1,5 +1,8 @@
use std::path::PathBuf;
use std::str::FromStr;
use ui::prelude::*; use ui::prelude::*;
use ui::Breadcrumb; use ui::{Breadcrumb, HighlightedText, Symbol};
use crate::story::Story; use crate::story::Story;
@ -8,9 +11,35 @@ pub struct BreadcrumbStory {}
impl BreadcrumbStory { impl BreadcrumbStory {
fn render<V: 'static>(&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);
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, Breadcrumb>(cx)) .child(Story::title_for::<_, Breadcrumb>(cx))
.child(Story::label(cx, "Default")) .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),
},
]),
],
))
} }
} }

View file

@ -12,8 +12,10 @@ pub struct BufferStory {}
impl BufferStory { impl BufferStory {
fn render<V: 'static>(&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);
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, Buffer<V>>(cx)) .child(Story::title_for::<_, Buffer>(cx))
.child(Story::label(cx, "Default")) .child(Story::label(cx, "Default"))
.child(div().w(rems(64.)).h_96().child(empty_buffer_example())) .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
.child(Story::label(cx, "Hello World (Rust)")) .child(Story::label(cx, "Hello World (Rust)"))
@ -21,14 +23,14 @@ impl BufferStory {
div() div()
.w(rems(64.)) .w(rems(64.))
.h_96() .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(Story::label(cx, "Hello World (Rust) with Status"))
.child( .child(
div() div()
.w(rems(64.)) .w(rems(64.))
.h_96() .h_96()
.child(hello_world_rust_buffer_with_status_example(cx)), .child(hello_world_rust_buffer_with_status_example(&theme)),
) )
} }
} }

View file

@ -1,6 +1,6 @@
use chrono::DateTime; use chrono::DateTime;
use ui::prelude::*; use ui::prelude::*;
use ui::{ChatMessage, ChatPanel}; use ui::{ChatMessage, ChatPanel, Panel};
use crate::story::Story; use crate::story::Story;
@ -12,9 +12,17 @@ impl ChatPanelStory {
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, ChatPanel<V>>(cx)) .child(Story::title_for::<_, ChatPanel<V>>(cx))
.child(Story::label(cx, "Default")) .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(Story::label(cx, "With Mesages"))
.child(ChatPanel::new(ScrollState::default()).with_messages(vec![ .child(Panel::new(
ScrollState::default(),
|_, _| {
vec![ChatPanel::new(ScrollState::default())
.with_messages(vec![
ChatMessage::new( ChatMessage::new(
"osiewicz".to_string(), "osiewicz".to_string(),
"is this thing on?".to_string(), "is this thing on?".to_string(),
@ -29,6 +37,10 @@ impl ChatPanelStory {
.unwrap() .unwrap()
.naive_local(), .naive_local(),
), ),
])) ])
.into_any()]
},
Box::new(()),
))
} }
} }

View file

@ -11,7 +11,7 @@ impl FacepileStory {
let players = static_players(); let players = static_players();
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, ui::Facepile>(cx)) .child(Story::title_for::<_, Facepile>(cx))
.child(Story::label(cx, "Default")) .child(Story::label(cx, "Default"))
.child( .child(
div() div()

View file

@ -14,9 +14,10 @@ impl PanelStory {
.child(Panel::new( .child(Panel::new(
ScrollState::default(), ScrollState::default(),
|_, _| { |_, _| {
(0..100) vec![div()
.map(|ix| Label::new(format!("Item {}", ix + 1)).into_any()) .overflow_y_scroll(ScrollState::default())
.collect() .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
.into_any()]
}, },
Box::new(()), Box::new(()),
)) ))

View file

@ -1,5 +1,5 @@
use ui::prelude::*; use ui::prelude::*;
use ui::ProjectPanel; use ui::{Panel, ProjectPanel};
use crate::story::Story; use crate::story::Story;
@ -11,6 +11,10 @@ impl ProjectPanelStory {
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, ProjectPanel<V>>(cx)) .child(Story::title_for::<_, ProjectPanel<V>>(cx))
.child(Story::label(cx, "Default")) .child(Story::label(cx, "Default"))
.child(ProjectPanel::new(ScrollState::default())) .child(Panel::new(
ScrollState::default(),
|_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
Box::new(()),
))
} }
} }

View file

@ -1,5 +1,5 @@
use ui::prelude::*; use ui::prelude::*;
use ui::TabBar; use ui::{Tab, TabBar};
use crate::story::Story; use crate::story::Story;
@ -11,6 +11,36 @@ impl TabBarStory {
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, TabBar<V>>(cx)) .child(Story::title_for::<_, TabBar<V>>(cx))
.child(Story::label(cx, "Default")) .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),
]))
} }
} }

View file

@ -1,5 +1,9 @@
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use ui::prelude::*; use ui::prelude::*;
use ui::Toolbar; use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar};
use crate::story::Story; use crate::story::Story;
@ -8,9 +12,59 @@ pub struct ToolbarStory {}
impl ToolbarStory { impl ToolbarStory {
fn render<V: 'static>(&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);
struct LeftItemsPayload {
pub theme: Arc<Theme>,
}
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, Toolbar>(cx)) .child(Story::title_for::<_, Toolbar<V>>(cx))
.child(Story::label(cx, "Default")) .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(()),
))
} }
} }

View file

@ -9,7 +9,7 @@ pub struct AvatarStory {}
impl AvatarStory { impl AvatarStory {
fn render<V: 'static>(&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> {
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, ui::Avatar>(cx)) .child(Story::title_for::<_, Avatar>(cx))
.child(Story::label(cx, "Default")) .child(Story::label(cx, "Default"))
.child(Avatar::new( .child(Avatar::new(
"https://avatars.githubusercontent.com/u/1714999?v=4", "https://avatars.githubusercontent.com/u/1714999?v=4",

View file

@ -12,7 +12,7 @@ impl IconStory {
let icons = Icon::iter(); let icons = Icon::iter();
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, ui::IconElement>(cx)) .child(Story::title_for::<_, IconElement>(cx))
.child(Story::label(cx, "All Icons")) .child(Story::label(cx, "All Icons"))
.child(div().flex().gap_3().children(icons.map(IconElement::new))) .child(div().flex().gap_3().children(icons.map(IconElement::new)))
} }

View file

@ -19,5 +19,8 @@ impl KitchenSinkStory {
.child(div().flex().flex_col().children_any(element_stories)) .child(div().flex().flex_col().children_any(element_stories))
.child(Story::label(cx, "Components")) .child(Story::label(cx, "Components"))
.child(div().flex().flex_col().children_any(component_stories)) .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())
} }
} }

View file

@ -4,7 +4,7 @@ mod stories;
mod story; mod story;
mod story_selector; mod story_selector;
use std::sync::Arc; use std::{process::Command, sync::Arc};
use ::theme as legacy_theme; use ::theme as legacy_theme;
use clap::Parser; use clap::Parser;
@ -38,11 +38,44 @@ struct Args {
theme: Option<String>, 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() { fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
let args = Args::parse(); let args = Args::parse();
let fs = Arc::new(fs::RealFs);
gpui2::App::new(Assets).unwrap().run(move |cx| { gpui2::App::new(Assets).unwrap().run(move |cx| {
let mut store = SettingsStore::default(); let mut store = SettingsStore::default();
store store
@ -63,6 +96,10 @@ fn main() {
}) })
.and_then(|theme_name| theme_registry.get(&theme_name).ok()); .and_then(|theme_name| theme_registry.get(&theme_name).ok());
cx.spawn(|_| async move {
watch_zed_changes(fs).await;
})
.detach();
cx.add_window( cx.add_window(
gpui2::WindowOptions { gpui2::WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))), bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),

View file

@ -69,6 +69,7 @@ pub enum ActivateScript {
Default, Default,
Csh, Csh,
Fish, Fish,
Nushell,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

View file

@ -131,6 +131,7 @@ pub struct Titlebar {
pub menu: TitlebarMenu, pub menu: TitlebarMenu,
pub project_menu_button: Toggleable<Interactive<ContainedText>>, pub project_menu_button: Toggleable<Interactive<ContainedText>>,
pub git_menu_button: Toggleable<Interactive<ContainedText>>, pub git_menu_button: Toggleable<Interactive<ContainedText>>,
pub project_host: Interactive<ContainedText>,
pub item_spacing: f32, pub item_spacing: f32,
pub face_pile_spacing: f32, pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon, pub avatar_ribbon: AvatarRibbon,
@ -1065,13 +1066,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
} }
impl Editor { impl Editor {
pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle { pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle {
let style_ix = replica_id as usize % (self.guest_selections.len() + 1); if self.guest_selections.is_empty() {
if style_ix == 0 { return SelectionStyle::default();
&self.selection
} else {
&self.guest_selections[style_ix - 1]
} }
let style_ix = participant_index as usize % self.guest_selections.len();
self.guest_selections[style_ix]
} }
} }

View file

@ -13,3 +13,4 @@ settings = { path = "../settings" }
smallvec.workspace = true smallvec.workspace = true
strum = { version = "0.25.0", features = ["derive"] } strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" } theme = { path = "../theme" }
rand = "0.8"

View file

@ -5,7 +5,7 @@ mod chat_panel;
mod collab_panel; mod collab_panel;
mod command_palette; mod command_palette;
mod context_menu; mod context_menu;
mod editor; mod editor_pane;
mod facepile; mod facepile;
mod icon_button; mod icon_button;
mod keybinding; mod keybinding;
@ -31,7 +31,7 @@ pub use chat_panel::*;
pub use collab_panel::*; pub use collab_panel::*;
pub use command_palette::*; pub use command_palette::*;
pub use context_menu::*; pub use context_menu::*;
pub use editor::*; pub use editor_pane::*;
pub use facepile::*; pub use facepile::*;
pub use icon_button::*; pub use icon_button::*;
pub use keybinding::*; pub use keybinding::*;

View file

@ -1,17 +1,35 @@
use crate::prelude::*; use std::path::PathBuf;
use gpui2::elements::div::Div;
use crate::{h_stack, theme}; use crate::{h_stack, theme};
use crate::{prelude::*, HighlightedText};
#[derive(Clone)]
pub struct Symbol(pub Vec<HighlightedText>);
#[derive(Element)] #[derive(Element)]
pub struct Breadcrumb {} pub struct Breadcrumb {
path: PathBuf,
symbols: Vec<Symbol>,
}
impl Breadcrumb { impl Breadcrumb {
pub fn new() -> Self { pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
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> { fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let symbols_len = self.symbols.len();
h_stack() h_stack()
.px_1() .px_1()
// TODO: Read font from theme (or settings?). // TODO: Read font from theme (or settings?).
@ -21,11 +39,33 @@ impl Breadcrumb {
.rounded_md() .rounded_md()
.hover() .hover()
.fill(theme.highest.base.hovered.background) .fill(theme.highest.base.hovered.background)
// TODO: Replace hardcoded breadcrumbs. .child(self.path.clone().to_str().unwrap().to_string())
.child("crates/ui/src/components/toolbar.rs") .child(if !self.symbols.is_empty() {
.child(" ") self.render_separator(&theme)
.child("impl Breadcrumb") } else {
.child(" ") div()
.child("fn render") })
.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<_>>(),
),
)
} }
} }

View file

@ -1,5 +1,3 @@
use std::marker::PhantomData;
use gpui2::{Hsla, WindowContext}; use gpui2::{Hsla, WindowContext};
use crate::prelude::*; use crate::prelude::*;
@ -33,6 +31,7 @@ pub struct BufferRow {
pub show_line_number: bool, pub show_line_number: bool,
} }
#[derive(Clone)]
pub struct BufferRows { pub struct BufferRows {
pub show_line_numbers: bool, pub show_line_numbers: bool,
pub rows: Vec<BufferRow>, pub rows: Vec<BufferRow>,
@ -108,9 +107,8 @@ impl BufferRow {
} }
} }
#[derive(Element)] #[derive(Element, Clone)]
pub struct Buffer<V: 'static> { pub struct Buffer {
view_type: PhantomData<V>,
scroll_state: ScrollState, scroll_state: ScrollState,
rows: Option<BufferRows>, rows: Option<BufferRows>,
readonly: bool, readonly: bool,
@ -119,10 +117,9 @@ pub struct Buffer<V: 'static> {
path: Option<String>, path: Option<String>,
} }
impl<V: 'static> Buffer<V> { impl Buffer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
view_type: PhantomData,
scroll_state: ScrollState::default(), scroll_state: ScrollState::default(),
rows: Some(BufferRows::default()), rows: Some(BufferRows::default()),
readonly: false, readonly: false,
@ -161,7 +158,7 @@ impl<V: 'static> Buffer<V> {
self 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 theme = theme(cx);
let system_color = SystemColor::new(); let system_color = SystemColor::new();
@ -172,28 +169,35 @@ impl<V: 'static> Buffer<V> {
}; };
let line_number_color = if row.current { let line_number_color = if row.current {
HighlightColor::Default.hsla(cx) HighlightColor::Default.hsla(&theme)
} else { } else {
HighlightColor::Comment.hsla(cx) HighlightColor::Comment.hsla(&theme)
}; };
h_stack() h_stack()
.fill(line_background) .fill(line_background)
.w_full()
.gap_2() .gap_2()
.px_2() .px_1()
.child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| { .child(
h_stack()
.w_4()
.h_full()
.px_0p5()
.when(row.code_action, |c| {
div().child(IconElement::new(Icon::Bolt)) div().child(IconElement::new(Icon::Bolt))
})) }),
)
.when(row.show_line_number, |this| { .when(row.show_line_number, |this| {
this.child( this.child(
h_stack().justify_end().px_1().w_4().child( h_stack().justify_end().px_0p5().w_3().child(
div() div()
.text_color(line_number_color) .text_color(line_number_color)
.child(row.line_number.to_string()), .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| { .children(row.line.map(|line| {
div() div()
.flex() .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 { match &self.rows {
Some(rows) => rows Some(rows) => 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 theme = theme(cx);
let rows = self.render_rows(cx); let rows = self.render_rows(cx);
v_stack() v_stack()

View file

@ -4,13 +4,12 @@ use chrono::NaiveDateTime;
use crate::prelude::*; use crate::prelude::*;
use crate::theme::theme; use crate::theme::theme;
use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide}; use crate::{Icon, IconButton, Input, Label, LabelColor};
#[derive(Element)] #[derive(Element)]
pub struct ChatPanel<V: 'static> { pub struct ChatPanel<V: 'static> {
view_type: PhantomData<V>, view_type: PhantomData<V>,
scroll_state: ScrollState, scroll_state: ScrollState,
current_side: PanelSide,
messages: Vec<ChatMessage>, messages: Vec<ChatMessage>,
} }
@ -19,16 +18,10 @@ impl<V: 'static> ChatPanel<V> {
Self { Self {
view_type: PhantomData, view_type: PhantomData,
scroll_state, scroll_state,
current_side: PanelSide::default(),
messages: Vec::new(), 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 { pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
self.messages = messages; self.messages = messages;
self self
@ -37,19 +30,10 @@ impl<V: 'static> ChatPanel<V> {
fn render(&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); let theme = theme(cx);
struct PanelPayload { div()
pub scroll_state: ScrollState,
pub messages: Vec<ChatMessage>,
}
Panel::new(
self.scroll_state.clone(),
|_, payload| {
let payload = payload.downcast_ref::<PanelPayload>().unwrap();
vec![div()
.flex() .flex()
.flex_col() .flex_col()
.justify_between()
.h_full() .h_full()
.px_2() .px_2()
.gap_2() .gap_2()
@ -58,7 +42,7 @@ impl<V: 'static> ChatPanel<V> {
div() div()
.flex() .flex()
.justify_between() .justify_between()
.gap_2() .py_2()
.child(div().flex().child(Label::new("#design"))) .child(div().flex().child(Label::new("#design")))
.child( .child(
div() div()
@ -69,6 +53,10 @@ impl<V: 'static> ChatPanel<V> {
.child(IconButton::new(Icon::AudioOn)), .child(IconButton::new(Icon::AudioOn)),
), ),
) )
.child(
div()
.flex()
.flex_col()
// Chat Body // Chat Body
.child( .child(
div() div()
@ -76,19 +64,12 @@ impl<V: 'static> ChatPanel<V> {
.flex() .flex()
.flex_col() .flex_col()
.gap_3() .gap_3()
.overflow_y_scroll(payload.scroll_state.clone()) .overflow_y_scroll(self.scroll_state.clone())
.children(payload.messages.clone()), .children(self.messages.clone()),
) )
// Composer // Composer
.child(div().flex().gap_2().child(Input::new("Message #design"))) .child(div().flex().my_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)
} }
} }

View file

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

View file

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

View file

@ -105,16 +105,12 @@ impl<V: 'static> Panel<V> {
let theme = theme(cx); let theme = theme(cx);
let panel_base; let panel_base;
let current_width = if let Some(width) = self.width { let current_width = self.width.unwrap_or(self.initial_width);
width
} else {
self.initial_width
};
match self.current_side { match self.current_side {
PanelSide::Left => { PanelSide::Left => {
panel_base = v_stack() panel_base = v_stack()
.overflow_y_scroll(self.scroll_state.clone()) .flex_initial()
.h_full() .h_full()
.w(current_width) .w(current_width)
.fill(theme.middle.base.default.background) .fill(theme.middle.base.default.background)
@ -123,20 +119,20 @@ impl<V: 'static> Panel<V> {
} }
PanelSide::Right => { PanelSide::Right => {
panel_base = v_stack() panel_base = v_stack()
.overflow_y_scroll(self.scroll_state.clone()) .flex_initial()
.h_full() .h_full()
.w(current_width) .w(current_width)
.fill(theme.middle.base.default.background) .fill(theme.middle.base.default.background)
.border_r() .border_l()
.border_color(theme.middle.base.default.border); .border_color(theme.middle.base.default.border);
} }
PanelSide::Bottom => { PanelSide::Bottom => {
panel_base = v_stack() panel_base = v_stack()
.overflow_y_scroll(self.scroll_state.clone()) .flex_initial()
.w_full() .w_full()
.h(current_width) .h(current_width)
.fill(theme.middle.base.default.background) .fill(theme.middle.base.default.background)
.border_r() .border_t()
.border_color(theme.middle.base.default.border); .border_color(theme.middle.base.default.border);
} }
} }

View file

@ -38,9 +38,8 @@ impl PlayerStack {
div().flex().justify_center().w_full().child( div().flex().justify_center().w_full().child(
div() div()
.w_4() .w_4()
.h_1() .h_0p5()
.rounded_bl_sm() .rounded_sm()
.rounded_br_sm()
.fill(player.cursor_color(cx)), .fill(player.cursor_color(cx)),
), ),
) )
@ -50,7 +49,7 @@ impl PlayerStack {
.items_center() .items_center()
.justify_center() .justify_center()
.h_6() .h_6()
.px_1() .pl_1()
.rounded_lg() .rounded_lg()
.fill(if followers.is_none() { .fill(if followers.is_none() {
system_color.transparent system_color.transparent
@ -59,7 +58,7 @@ impl PlayerStack {
}) })
.child(Avatar::new(player.avatar_src().to_string())) .child(Avatar::new(player.avatar_src().to_string()))
.children(followers.map(|followers| { .children(followers.map(|followers| {
div().neg_mr_1().child(Facepile::new(followers.into_iter())) div().neg_ml_2().child(Facepile::new(followers.into_iter()))
})), })),
) )
} }

View file

@ -1,17 +1,15 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc;
use crate::prelude::*; use crate::prelude::*;
use crate::{ use crate::{
static_project_panel_project_items, static_project_panel_single_items, theme, Input, List, static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
ListHeader, Panel, PanelSide, Theme, ListHeader,
}; };
#[derive(Element)] #[derive(Element)]
pub struct ProjectPanel<V: 'static> { pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>, view_type: PhantomData<V>,
scroll_state: ScrollState, scroll_state: ScrollState,
current_side: PanelSide,
} }
impl<V: 'static> ProjectPanel<V> { impl<V: 'static> ProjectPanel<V> {
@ -19,32 +17,16 @@ impl<V: 'static> ProjectPanel<V> {
Self { Self {
view_type: PhantomData, view_type: PhantomData,
scroll_state, 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> { fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
struct PanelPayload { let theme = theme(cx);
pub theme: Arc<Theme>,
pub scroll_state: ScrollState,
}
Panel::new( div()
self.scroll_state.clone(),
|_, payload| {
let payload = payload.downcast_ref::<PanelPayload>().unwrap();
let theme = payload.theme.clone();
vec![div()
.flex() .flex()
.flex_col() .flex_col()
.w_56() .w_full()
.h_full() .h_full()
.px_2() .px_2()
.fill(theme.middle.base.default.background) .fill(theme.middle.base.default.background)
@ -53,20 +35,16 @@ impl<V: 'static> ProjectPanel<V> {
.w_56() .w_56()
.flex() .flex()
.flex_col() .flex_col()
.overflow_y_scroll(payload.scroll_state.clone()) .overflow_y_scroll(ScrollState::default())
.child( .child(
List::new(static_project_panel_single_items()) List::new(static_project_panel_single_items())
.header( .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
ListHeader::new("FILES").set_toggle(ToggleState::Toggled),
)
.empty_message("No files in directory") .empty_message("No files in directory")
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
) )
.child( .child(
List::new(static_project_panel_project_items()) List::new(static_project_panel_project_items())
.header( .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled),
)
.empty_message("No folders in directory") .empty_message("No folders in directory")
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
), ),
@ -76,12 +54,5 @@ impl<V: 'static> ProjectPanel<V> {
.value("buffe".to_string()) .value("buffe".to_string())
.state(InteractionState::Focused), .state(InteractionState::Focused),
) )
.into_any()]
},
Box::new(PanelPayload {
theme: theme(cx),
scroll_state: self.scroll_state.clone(),
}),
)
} }
} }

View file

@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
#[derive(Element)] #[derive(Element, Clone)]
pub struct Tab { pub struct Tab {
title: String, title: String,
icon: Option<Icon>, icon: Option<Icon>,

View file

@ -7,20 +7,27 @@ use crate::{theme, Icon, IconButton, Tab};
pub struct TabBar<V: 'static> { pub struct TabBar<V: 'static> {
view_type: PhantomData<V>, view_type: PhantomData<V>,
scroll_state: ScrollState, scroll_state: ScrollState,
tabs: Vec<Tab>,
} }
impl<V: 'static> TabBar<V> { impl<V: 'static> TabBar<V> {
pub fn new(scroll_state: ScrollState) -> Self { pub fn new(tabs: Vec<Tab>) -> Self {
Self { Self {
view_type: PhantomData, 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> { fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let can_navigate_back = true; let can_navigate_back = true;
let can_navigate_forward = false; let can_navigate_forward = false;
div() div()
.w_full() .w_full()
.flex() .flex()
@ -54,51 +61,7 @@ impl<V: 'static> TabBar<V> {
div() div()
.flex() .flex()
.overflow_x_scroll(self.scroll_state.clone()) .overflow_x_scroll(self.scroll_state.clone())
.child( .children(self.tabs.clone()),
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),
),
), ),
) )
// Right Side // Right Side

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use gpui2::geometry::{relative, rems, Size}; use gpui2::geometry::{relative, rems, Size};
use crate::prelude::*; use crate::prelude::*;
@ -20,6 +22,7 @@ impl Terminal {
div() div()
.flex() .flex()
.flex_col() .flex_col()
.w_full()
.child( .child(
// Terminal Tabs. // Terminal Tabs.
div() div()
@ -70,8 +73,12 @@ impl Terminal {
width: relative(1.).into(), width: relative(1.).into(),
height: rems(36.).into(), height: rems(36.).into(),
}, },
|_, _| vec![], |_, payload| {
Box::new(()), let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
vec![crate::static_data::terminal_buffer(&theme).into_any()]
},
Box::new(theme),
)) ))
} }
} }

View file

@ -2,16 +2,24 @@ use std::marker::PhantomData;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use crate::prelude::*; use crate::{prelude::*, PlayerWithCallStatus};
use crate::{ use crate::{
static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights,
PlayerStack, ToolDivider, TrafficLights,
}; };
#[derive(Clone)]
pub struct Livestream {
pub players: Vec<PlayerWithCallStatus>,
pub channel: Option<String>, // projects
// windows
}
#[derive(Element)] #[derive(Element)]
pub struct TitleBar<V: 'static> { pub struct TitleBar<V: 'static> {
view_type: PhantomData<V>, view_type: PhantomData<V>,
/// If the window is active from the OS's perspective.
is_active: Arc<AtomicBool>, is_active: Arc<AtomicBool>,
livestream: Option<Livestream>,
} }
impl<V: 'static> TitleBar<V> { impl<V: 'static> TitleBar<V> {
@ -28,14 +36,24 @@ impl<V: 'static> TitleBar<V> {
Self { Self {
view_type: PhantomData, view_type: PhantomData,
is_active, 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> { fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx); let theme = theme(cx);
let has_focus = cx.window_is_active(); 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() div()
.flex() .flex()
@ -61,7 +79,8 @@ impl<V: 'static> TitleBar<V> {
.child(Button::new("zed")) .child(Button::new("zed"))
.child(Button::new("nate/gpui2-ui-components")), .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( .child(
div() div()

View file

@ -1,33 +1,49 @@
use crate::prelude::*; use crate::prelude::*;
use crate::{theme, Breadcrumb, Icon, IconButton}; use crate::theme;
#[derive(Clone)] #[derive(Clone)]
pub struct ToolbarItem {} pub struct ToolbarItem {}
#[derive(Element, Clone)] #[derive(Element)]
pub struct Toolbar { pub struct Toolbar<V: 'static> {
items: Vec<ToolbarItem>, left_items: HackyChildren<V>,
left_items_payload: HackyChildrenPayload,
right_items: HackyChildren<V>,
right_items_payload: HackyChildrenPayload,
} }
impl Toolbar { impl<V: 'static> Toolbar<V> {
pub fn new() -> Self { pub fn new(
Self { items: Vec::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); let theme = theme(cx);
div() div()
.fill(theme.highest.base.default.background)
.p_2() .p_2()
.flex() .flex()
.justify_between() .justify_between()
.child(Breadcrumb::new())
.child( .child(
div() div()
.flex() .flex()
.child(IconButton::new(Icon::InlayHint)) .children_any((self.left_items)(cx, self.left_items_payload.as_ref())),
.child(IconButton::new(Icon::MagnifyingGlass)) )
.child(IconButton::new(Icon::MagicWand)), .child(
div()
.flex()
.children_any((self.right_items)(cx, self.right_items_payload.as_ref())),
) )
} }
} }

View file

@ -1,10 +1,15 @@
use std::sync::Arc;
use chrono::DateTime; use chrono::DateTime;
use gpui2::geometry::{relative, rems, Size}; use gpui2::geometry::{relative, rems, Size};
use crate::prelude::*;
use crate::{ use crate::{
theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status,
ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, Livestream,
};
use crate::{
theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides,
PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
}; };
#[derive(Element, Default)] #[derive(Element, Default)]
@ -17,6 +22,8 @@ pub struct WorkspaceElement {
impl WorkspaceElement { impl WorkspaceElement {
fn render<V: 'static>(&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).clone();
let temp_size = rems(36.).into(); let temp_size = rems(36.).into();
let root_group = PaneGroup::new_groups( let root_group = PaneGroup::new_groups(
@ -29,8 +36,15 @@ impl WorkspaceElement {
width: relative(1.).into(), width: relative(1.).into(),
height: temp_size, height: temp_size,
}, },
|_, _| vec![Terminal::new().into_any()], |_, payload| {
Box::new(()), 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( Pane::new(
ScrollState::default(), ScrollState::default(),
@ -51,8 +65,15 @@ impl WorkspaceElement {
width: relative(1.).into(), width: relative(1.).into(),
height: relative(1.).into(), height: relative(1.).into(),
}, },
|_, _| vec![Terminal::new().into_any()], |_, payload| {
Box::new(()), 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, SplitDirection::Vertical,
), ),
@ -60,8 +81,6 @@ impl WorkspaceElement {
SplitDirection::Horizontal, SplitDirection::Horizontal,
); );
let theme = theme(cx).clone();
div() div()
.size_full() .size_full()
.flex() .flex()
@ -72,7 +91,10 @@ impl WorkspaceElement {
.items_start() .items_start()
.text_color(theme.lowest.base.default.foreground) .text_color(theme.lowest.base.default.foreground)
.fill(theme.lowest.base.default.background) .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( .child(
div() div()
.flex_1() .flex_1()
@ -84,7 +106,11 @@ impl WorkspaceElement {
.border_b() .border_b()
.border_color(theme.lowest.base.default.border) .border_color(theme.lowest.base.default.border)
.child( .child(
ProjectPanel::new(self.left_panel_scroll_state.clone()) Panel::new(
self.left_panel_scroll_state.clone(),
|_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
Box::new(()),
)
.side(PanelSide::Left), .side(PanelSide::Left),
) )
.child( .child(
@ -110,7 +136,12 @@ impl WorkspaceElement {
.side(PanelSide::Bottom), .side(PanelSide::Bottom),
), ),
) )
.child(ChatPanel::new(ScrollState::default()).with_messages(vec![ .child(
Panel::new(
self.right_panel_scroll_state.clone(),
|_, payload| {
vec![ChatPanel::new(ScrollState::default())
.with_messages(vec![
ChatMessage::new( ChatMessage::new(
"osiewicz".to_string(), "osiewicz".to_string(),
"is this thing on?".to_string(), "is this thing on?".to_string(),
@ -129,7 +160,13 @@ impl WorkspaceElement {
.unwrap() .unwrap()
.naive_local(), .naive_local(),
), ),
])), ])
.into_any()]
},
Box::new(()),
)
.side(PanelSide::Right),
),
) )
.child(StatusBar::new()) .child(StatusBar::new())
} }

View file

@ -84,6 +84,7 @@ pub enum Icon {
Plus, Plus,
Quote, Quote,
Screen, Screen,
SelectAll,
Split, Split,
SplitMessage, SplitMessage,
Terminal, Terminal,
@ -131,6 +132,7 @@ impl Icon {
Icon::Plus => "icons/plus.svg", Icon::Plus => "icons/plus.svg",
Icon::Quote => "icons/quote.svg", Icon::Quote => "icons/quote.svg",
Icon::Screen => "icons/desktop.svg", Icon::Screen => "icons/desktop.svg",
Icon::SelectAll => "icons/select-all.svg",
Icon::Split => "icons/split.svg", Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg", Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg", Icon::Terminal => "icons/terminal.svg",

View file

@ -81,6 +81,7 @@ impl Input {
div() div()
.h_7() .h_7()
.w_full()
.px_2() .px_2()
.border() .border()
.border_color(border_color_default) .border_color(border_color_default)

View file

@ -65,7 +65,7 @@ impl PlayerCallStatus {
} }
} }
#[derive(Clone)] #[derive(PartialEq, Clone)]
pub struct Player { pub struct Player {
index: usize, index: usize,
avatar_src: String, avatar_src: String,
@ -73,6 +73,7 @@ pub struct Player {
status: PlayerStatus, status: PlayerStatus,
} }
#[derive(Clone)]
pub struct PlayerWithCallStatus { pub struct PlayerWithCallStatus {
player: Player, player: Player,
call_status: PlayerCallStatus, call_status: PlayerCallStatus,

View file

@ -2,7 +2,7 @@ pub use gpui2::elements::div::{div, ScrollState};
pub use gpui2::style::{StyleHelpers, Styleable}; pub use gpui2::style::{StyleHelpers, Styleable};
pub use gpui2::{Element, IntoElement, ParentElement, ViewContext}; 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 gpui2::{hsla, rgb, Hsla, WindowContext};
use strum::EnumIter; use strum::EnumIter;
@ -40,8 +40,7 @@ pub enum HighlightColor {
} }
impl HighlightColor { impl HighlightColor {
pub fn hsla(&self, cx: &WindowContext) -> Hsla { pub fn hsla(&self, theme: &Theme) -> Hsla {
let theme = theme(cx);
let system_color = SystemColor::new(); let system_color = SystemColor::new();
match self { 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 { pub enum FileSystemStatus {
#[default] #[default]
None, 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 { pub enum GitStatus {
#[default] #[default]
None, None,
@ -130,7 +129,7 @@ impl GitStatus {
} }
} }
#[derive(Default, PartialEq)] #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum DiagnosticStatus { pub enum DiagnosticStatus {
#[default] #[default]
None, None,
@ -139,14 +138,14 @@ pub enum DiagnosticStatus {
Info, Info,
} }
#[derive(Default, PartialEq)] #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum IconSide { pub enum IconSide {
#[default] #[default]
Left, Left,
Right, Right,
} }
#[derive(Default, PartialEq)] #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum OrderMethod { pub enum OrderMethod {
#[default] #[default]
Ascending, Ascending,
@ -154,14 +153,14 @@ pub enum OrderMethod {
MostRecent, MostRecent,
} }
#[derive(Default, PartialEq, Clone, Copy)] #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum Shape { pub enum Shape {
#[default] #[default]
Circle, Circle,
RoundedRectangle, RoundedRectangle,
} }
#[derive(Default, PartialEq, Clone, Copy)] #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum DisclosureControlVisibility { pub enum DisclosureControlVisibility {
#[default] #[default]
OnHover, OnHover,

View file

@ -1,12 +1,109 @@
use gpui2::WindowContext; use std::path::PathBuf;
use std::str::FromStr;
use rand::Rng;
use crate::{ use crate::{
Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText, Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus, HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry,
ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player,
ToggleState, 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> { pub fn static_players() -> Vec<Player> {
vec![ vec![
Player::new( 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> { pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
let players = static_players(); let players = static_players();
let mut player_0_status = PlayerCallStatus::new(); 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()) .left_icon(Icon::FolderOpen.into())
.indent_level(3) .indent_level(3)
.set_toggle(ToggleState::Toggled), .set_toggle(ToggleState::Toggled),
ListEntry::new(Label::new("derrive_element.rs")) ListEntry::new(Label::new("derive_element.rs"))
.left_icon(Icon::FileRust.into()) .left_icon(Icon::FileRust.into())
.indent_level(4), .indent_level(4),
ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) 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())) Buffer::new().set_rows(Some(BufferRows::default()))
} }
pub fn hello_world_rust_buffer_example<V: 'static>(cx: &WindowContext) -> Buffer<V> { 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_example(theme: &Theme) -> Buffer {
Buffer::new() Buffer::new()
.set_title("hello_world.rs".to_string()) .set_title("hello_world.rs".to_string())
.set_path("src/hello_world.rs".to_string()) .set_path("src/hello_world.rs".to_string())
.set_language("rust".to_string()) .set_language("rust".to_string())
.set_rows(Some(BufferRows { .set_rows(Some(BufferRows {
show_line_numbers: true, show_line_numbers: true,
rows: hello_world_rust_buffer_rows(cx), rows: hello_world_rust_buffer_rows(theme),
})) }))
} }
pub fn hello_world_rust_buffer_with_status_example<V: 'static>(cx: &WindowContext) -> Buffer<V> { pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
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),
}))
}
pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
let show_line_number = true; let show_line_number = true;
vec![ vec![
@ -375,15 +636,15 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
highlighted_texts: vec![ highlighted_texts: vec![
HighlightedText { HighlightedText {
text: "fn ".to_string(), text: "fn ".to_string(),
color: HighlightColor::Keyword.hsla(cx), color: HighlightColor::Keyword.hsla(&theme),
}, },
HighlightedText { HighlightedText {
text: "main".to_string(), text: "main".to_string(),
color: HighlightColor::Function.hsla(cx), color: HighlightColor::Function.hsla(&theme),
}, },
HighlightedText { HighlightedText {
text: "() {".to_string(), 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 { highlighted_texts: vec![HighlightedText {
text: " // Statements here are executed when the compiled binary is called." text: " // Statements here are executed when the compiled binary is called."
.to_string(), .to_string(),
color: HighlightColor::Comment.hsla(cx), color: HighlightColor::Comment.hsla(&theme),
}], }],
}), }),
cursors: None, cursors: None,
@ -422,7 +683,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
line: Some(HighlightedLine { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![HighlightedText {
text: " // Print text to the console.".to_string(), text: " // Print text to the console.".to_string(),
color: HighlightColor::Comment.hsla(cx), color: HighlightColor::Comment.hsla(&theme),
}], }],
}), }),
cursors: None, cursors: None,
@ -433,10 +694,34 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
line_number: 5, line_number: 5,
code_action: false, code_action: false,
current: 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 { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![HighlightedText {
text: "}".to_string(), text: "}".to_string(),
color: HighlightColor::Default.hsla(cx), color: HighlightColor::Default.hsla(&theme),
}], }],
}), }),
cursors: None, 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; let show_line_number = true;
vec![ vec![
@ -458,15 +772,15 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
highlighted_texts: vec![ highlighted_texts: vec![
HighlightedText { HighlightedText {
text: "fn ".to_string(), text: "fn ".to_string(),
color: HighlightColor::Keyword.hsla(cx), color: HighlightColor::Keyword.hsla(&theme),
}, },
HighlightedText { HighlightedText {
text: "main".to_string(), text: "main".to_string(),
color: HighlightColor::Function.hsla(cx), color: HighlightColor::Function.hsla(&theme),
}, },
HighlightedText { HighlightedText {
text: "() {".to_string(), 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 { highlighted_texts: vec![HighlightedText {
text: "// Statements here are executed when the compiled binary is called." text: "// Statements here are executed when the compiled binary is called."
.to_string(), .to_string(),
color: HighlightColor::Comment.hsla(cx), color: HighlightColor::Comment.hsla(&theme),
}], }],
}), }),
cursors: None, cursors: None,
@ -505,7 +819,7 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
line: Some(HighlightedLine { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![HighlightedText {
text: " // Print text to the console.".to_string(), text: " // Print text to the console.".to_string(),
color: HighlightColor::Comment.hsla(cx), color: HighlightColor::Comment.hsla(&theme),
}], }],
}), }),
cursors: None, cursors: None,
@ -517,10 +831,20 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
code_action: false, code_action: false,
current: false, current: false,
line: Some(HighlightedLine { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![
text: "}".to_string(), HighlightedText {
color: HighlightColor::Default.hsla(cx), 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, cursors: None,
status: GitStatus::None, status: GitStatus::None,
@ -532,12 +856,12 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
current: false, current: false,
line: Some(HighlightedLine { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![HighlightedText {
text: "".to_string(), text: "}".to_string(),
color: HighlightColor::Default.hsla(cx), color: HighlightColor::Default.hsla(&theme),
}], }],
}), }),
cursors: None, cursors: None,
status: GitStatus::Created, status: GitStatus::None,
show_line_number, show_line_number,
}, },
BufferRow { BufferRow {
@ -546,8 +870,22 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
current: false, current: false,
line: Some(HighlightedLine { line: Some(HighlightedLine {
highlighted_texts: vec![HighlightedText { highlighted_texts: vec![HighlightedText {
text: "Marshall and Nate were here".to_string(), text: "".to_string(),
color: HighlightColor::Default.hsla(cx), color: HighlightColor::Default.hsla(&theme),
}],
}),
cursors: None,
status: GitStatus::Created,
show_line_number,
},
BufferRow {
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::Comment.hsla(&theme),
}], }],
}), }),
cursors: None, 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,
},
]
}

View file

@ -171,7 +171,7 @@ impl Vim {
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
Event::SelectionsChanged { local: true } => { Event::SelectionsChanged { local: true } => {
let editor = editor.read(cx); 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); let newest = editor.selections.newest::<usize>(cx);
local_selections_changed(newest, cx); local_selections_changed(newest, cx);
} }
@ -195,6 +195,8 @@ impl Vim {
if editor_mode == EditorMode::Full if editor_mode == EditorMode::Full
&& !newest_selection_empty && !newest_selection_empty
&& self.state().mode == Mode::Normal && 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); self.switch_mode(Mode::Visual, true, cx);
} }

View file

@ -4,7 +4,10 @@ use crate::{
}; };
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result; use anyhow::Result;
use client::{proto, Client}; use client::{
proto::{self, PeerId},
Client,
};
use gpui::geometry::vector::Vector2F; use gpui::geometry::vector::Vector2F;
use gpui::AnyWindowHandle; use gpui::AnyWindowHandle;
use gpui::{ 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(followed_item) = self.to_followable_item_handle(cx) {
if let Some(message) = followed_item.to_state_proto(cx) { if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers( workspace.update_followers(
followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View { proto::update_followers::Variant::CreateView(proto::View {
id: followed_item id: followed_item
.remote_id(&workspace.app_state.client, cx) .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) { 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); let leader_id = workspace.leader_for_pane(&pane);
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { 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| { move |this, cx| {
pending_update_scheduled.store(false, Ordering::SeqCst); pending_update_scheduled.store(false, Ordering::SeqCst);
this.update_followers( this.update_followers(
is_project_item,
proto::update_followers::Variant::UpdateView( proto::update_followers::Variant::UpdateView(
proto::UpdateView { proto::UpdateView {
id: item id: item
@ -692,14 +698,15 @@ pub trait FollowableItem: Item {
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>>; ) -> 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; fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
} }
pub trait FollowableItemHandle: ItemHandle { pub trait FollowableItemHandle: ItemHandle {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>; 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 to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto( fn add_event_to_update_proto(
&self, &self,
@ -714,6 +721,7 @@ pub trait FollowableItemHandle: ItemHandle {
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Task<Result<()>>; ) -> Task<Result<()>>;
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; 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> { 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) { fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
self.update(cx, |this, cx| { self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
this.set_leader_replica_id(leader_replica_id, cx)
})
} }
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> { fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
@ -765,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
false false
} }
} }
fn is_project_item(&self, cx: &AppContext) -> bool {
self.read(cx).is_project_item(cx)
}
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]

View file

@ -190,25 +190,23 @@ impl Member {
}) })
.and_then(|leader_id| { .and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx); let room = active_call?.read(cx).room()?.read(cx);
let collaborator = project.read(cx).collaborators().get(leader_id)?; room.remote_participant_for_peer_id(*leader_id)
let participant = room.remote_participant_for_peer_id(*leader_id)?;
Some((collaborator.replica_id, participant))
}); });
let border = if let Some((replica_id, _)) = leader.as_ref() { let mut leader_border = Border::default();
let leader_color = theme.editor.replica_selection_style(*replica_id).cursor; let mut leader_status_box = None;
let mut border = Border::all(theme.workspace.leader_border_width, leader_color); if let Some(leader) = &leader {
border 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 .color
.fade_out(1. - theme.workspace.leader_border_opacity); .fade_out(1. - theme.workspace.leader_border_opacity);
border.overlay = true; leader_border.overlay = true;
border
} else {
Border::default()
};
let leader_status_box = if let Some((_, leader)) = leader { leader_status_box = match leader.location {
match leader.location {
ParticipantLocation::SharedProject { ParticipantLocation::SharedProject {
project_id: leader_project_id, project_id: leader_project_id,
} => { } => {
@ -217,7 +215,6 @@ impl Member {
} else { } else {
let leader_user = leader.user.clone(); let leader_user = leader.user.clone();
let leader_user_id = leader.user.id; let leader_user_id = leader.user.id;
let app_state = Arc::downgrade(app_state);
Some( Some(
MouseEventHandler::new::<FollowIntoExternalProject, _>( MouseEventHandler::new::<FollowIntoExternalProject, _>(
pane.id(), pane.id(),
@ -225,7 +222,7 @@ impl Member {
|_, _| { |_, _| {
Label::new( Label::new(
format!( format!(
"Follow {} on their active project", "Follow {} to their active project",
leader_user.github_login, leader_user.github_login,
), ),
theme theme
@ -241,16 +238,14 @@ impl Member {
}, },
) )
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(app_state) = app_state.upgrade() {
crate::join_remote_project( crate::join_remote_project(
leader_project_id, leader_project_id,
leader_user_id, leader_user_id,
app_state, this.app_state().clone(),
cx, cx,
) )
.detach_and_log_err(cx); .detach_and_log_err(cx);
}
}) })
.aligned() .aligned()
.bottom() .bottom()
@ -289,13 +284,11 @@ impl Member {
.right() .right()
.into_any(), .into_any(),
), ),
}
} else {
None
}; };
}
Stack::new() Stack::new()
.with_child(pane_element.contained().with_border(border)) .with_child(pane_element.contained().with_border(leader_border))
.with_children(leader_status_box) .with_children(leader_status_box)
.into_any() .into_any()
} }

View file

@ -375,11 +375,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
}) })
.detach(); .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< type ProjectItemBuilders = HashMap<
@ -456,6 +451,7 @@ pub struct AppState {
pub client: Arc<Client>, pub client: Arc<Client>,
pub user_store: ModelHandle<UserStore>, pub user_store: ModelHandle<UserStore>,
pub channel_store: ModelHandle<ChannelStore>, pub channel_store: ModelHandle<ChannelStore>,
pub workspace_store: ModelHandle<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>, pub fs: Arc<dyn fs::Fs>,
pub build_window_options: pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>, fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@ -464,6 +460,19 @@ pub struct AppState {
pub background_actions: BackgroundActions, 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 { impl AppState {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> { 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 user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_store = let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); 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); theme::init((), cx);
client::init(&client, cx); client::init(&client, cx);
@ -491,6 +501,7 @@ impl AppState {
languages, languages,
user_store, user_store,
channel_store, channel_store,
workspace_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())), initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(), build_window_options: |_, _, _| Default::default(),
background_actions: || &[], background_actions: || &[],
@ -551,7 +562,6 @@ pub enum Event {
pub struct Workspace { pub struct Workspace {
weak_self: WeakViewHandle<Self>, weak_self: WeakViewHandle<Self>,
remote_entity_subscription: Option<client::Subscription>,
modal: Option<ActiveModal>, modal: Option<ActiveModal>,
zoomed: Option<AnyWeakViewHandle>, zoomed: Option<AnyWeakViewHandle>,
zoomed_position: Option<DockPosition>, zoomed_position: Option<DockPosition>,
@ -567,7 +577,6 @@ pub struct Workspace {
titlebar_item: Option<AnyViewHandle>, titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>, notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
leader_state: LeaderState,
follower_states_by_leader: FollowerStatesByLeader, follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>, last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool, window_edited: bool,
@ -593,11 +602,6 @@ pub struct ViewId {
pub id: u64, pub id: u64,
} }
#[derive(Default)]
struct LeaderState {
followers: HashSet<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>; type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)] #[derive(Default)]
@ -618,9 +622,8 @@ impl Workspace {
cx.observe(&project, |_, _, cx| cx.notify()).detach(); cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(&project, move |this, _, event, cx| { cx.subscribe(&project, move |this, _, event, cx| {
match event { match event {
project::Event::RemoteIdChanged(remote_id) => { project::Event::RemoteIdChanged(_) => {
this.update_window_title(cx); this.update_window_title(cx);
this.project_remote_id_changed(*remote_id, cx);
} }
project::Event::CollaboratorLeft(peer_id) => { project::Event::CollaboratorLeft(peer_id) => {
@ -675,6 +678,10 @@ impl Workspace {
cx.focus(&center_pane); cx.focus(&center_pane);
cx.emit(Event::PaneAdded(center_pane.clone())); 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 current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status(); let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn(|this, mut cx| async move { 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(), weak_self: weak_handle.clone(),
modal: None, modal: None,
zoomed: None, zoomed: None,
@ -781,12 +789,10 @@ impl Workspace {
status_bar, status_bar,
titlebar_item: None, titlebar_item: None,
notifications: Default::default(), notifications: Default::default(),
remote_entity_subscription: None,
left_dock, left_dock,
bottom_dock, bottom_dock,
right_dock, right_dock,
project: project.clone(), project: project.clone(),
leader_state: Default::default(),
follower_states_by_leader: Default::default(), follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(), last_leaders_by_pane: Default::default(),
window_edited: false, window_edited: false,
@ -799,10 +805,7 @@ impl Workspace {
leader_updates_tx, leader_updates_tx,
subscriptions, subscriptions,
pane_history_timestamp, 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( fn new_local(
@ -2506,43 +2509,24 @@ impl Workspace {
&self.active_pane &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>) { 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) { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
for state in states_by_pane.into_values() { for state in states_by_pane.into_values() {
for item in state.items_by_leader_view_id.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(); cx.notify();
} }
pub fn toggle_follow( fn start_following(
&mut self, &mut self,
leader_id: PeerId, leader_id: PeerId,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let pane = self.active_pane().clone(); 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 self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id); .insert(pane.downgrade(), leader_id);
self.follower_states_by_leader self.follower_states_by_leader
@ -2551,8 +2535,10 @@ impl Workspace {
.insert(pane.clone(), Default::default()); .insert(pane.clone(), Default::default());
cx.notify(); 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 { let request = self.app_state.client.request(proto::Follow {
room_id,
project_id, project_id,
leader_id: Some(leader_id), leader_id: Some(leader_id),
}); });
@ -2611,9 +2597,64 @@ impl Workspace {
None None
}; };
next_leader_id let pane = self.active_pane.clone();
.or_else(|| collaborators.keys().copied().next()) let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
.and_then(|leader_id| self.toggle_follow(leader_id, cx)) 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( pub fn unfollow(
@ -2625,21 +2666,22 @@ impl Workspace {
let leader_id = *leader_id; let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) { if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id { 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() { if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id); self.follower_states_by_leader.remove(&leader_id);
if let Some(project_id) = self.project.read(cx).remote_id() { let project_id = self.project.read(cx).remote_id();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
self.app_state self.app_state
.client .client
.send(proto::Unfollow { .send(proto::Unfollow {
room_id,
project_id, project_id,
leader_id: Some(leader_id), leader_id: Some(leader_id),
}) })
.log_err(); .log_err();
} }
}
cx.notify(); cx.notify();
return Some(leader_id); return Some(leader_id);
@ -2652,10 +2694,6 @@ impl Workspace {
self.follower_states_by_leader.contains_key(&peer_id) 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> { fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// TODO: There should be a better system in place for this // TODO: There should be a better system in place for this
// (https://github.com/zed-industries/zed/issues/1290) // (https://github.com/zed-industries/zed/issues/1290)
@ -2806,19 +2844,15 @@ impl Workspace {
// RPC handlers // RPC handlers
async fn handle_follow( fn handle_follow(
this: WeakViewHandle<Self>, &mut self,
envelope: TypedEnvelope<proto::Follow>, follower_project_id: Option<u64>,
_: Arc<Client>, cx: &mut ViewContext<Self>,
mut cx: AsyncAppContext, ) -> proto::FollowResponse {
) -> Result<proto::FollowResponse> { let client = &self.app_state.client;
this.update(&mut cx, |this, cx| { let project_id = self.project.read(cx).remote_id();
let client = &this.app_state.client;
this.leader_state
.followers
.insert(envelope.original_sender_id()?);
let active_view_id = this.active_item(cx).and_then(|i| { let active_view_id = self.active_item(cx).and_then(|i| {
Some( Some(
i.to_followable_item_handle(cx)? i.to_followable_item_handle(cx)?
.remote_id(client, cx)? .remote_id(client, cx)?
@ -2828,17 +2862,23 @@ impl Workspace {
cx.notify(); cx.notify();
Ok(proto::FollowResponse { proto::FollowResponse {
active_view_id, active_view_id,
views: this views: self
.panes() .panes()
.iter() .iter()
.flat_map(|pane| { .flat_map(|pane| {
let leader_id = this.leader_for_pane(pane); let leader_id = self.leader_for_pane(pane);
pane.read(cx).items().filter_map({ pane.read(cx).items().filter_map({
let cx = &cx; let cx = &cx;
move |item| { move |item| {
let item = item.to_followable_item_handle(cx)?; 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 id = item.remote_id(client, cx)?.to_proto();
let variant = item.to_state_proto(cx)?; let variant = item.to_state_proto(cx)?;
Some(proto::View { Some(proto::View {
@ -2850,37 +2890,18 @@ impl Workspace {
}) })
}) })
.collect(), .collect(),
}) }
})?
} }
async fn handle_unfollow( fn handle_update_followers(
this: WeakViewHandle<Self>, &mut self,
envelope: TypedEnvelope<proto::Unfollow>, leader_id: PeerId,
_: Arc<Client>, message: proto::UpdateFollowers,
mut cx: AsyncAppContext, _cx: &mut ViewContext<Self>,
) -> Result<()> { ) {
this.update(&mut cx, |this, cx| { self.leader_updates_tx
this.leader_state .unbounded_send((leader_id, message))
.followers .ok();
.remove(&envelope.original_sender_id()?);
cx.notify();
Ok(())
})?
}
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(())
} }
async fn process_leader_update( async fn process_leader_update(
@ -2953,18 +2974,6 @@ impl Workspace {
let this = this let this = this
.upgrade(cx) .upgrade(cx)
.ok_or_else(|| anyhow!("workspace dropped"))?; .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| { let item_builders = cx.update(|cx| {
cx.default_global::<FollowableItemBuilders>() cx.default_global::<FollowableItemBuilders>()
@ -3009,7 +3018,7 @@ impl Workspace {
.get_mut(&pane)?; .get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) { 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); state.items_by_leader_view_id.insert(id, item);
} }
@ -3020,46 +3029,44 @@ impl Workspace {
} }
fn update_active_view_for_followers(&self, cx: &AppContext) { 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() { if self.active_pane.read(cx).has_focus() {
self.update_followers( let item = self
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { .active_item(cx)
id: self.active_item(cx).and_then(|item| { .and_then(|item| item.to_followable_item_handle(cx));
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) .remote_id(&self.app_state.client, cx)
.map(|id| id.to_proto()) .map(|id| id.to_proto()),
}),
leader_id: self.leader_for_pane(&self.active_pane), 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( fn update_followers(
&self, &self,
project_only: bool,
update: proto::update_followers::Variant, update: proto::update_followers::Variant,
cx: &AppContext, cx: &AppContext,
) -> Option<()> { ) -> Option<()> {
let project_id = self.project.read(cx).remote_id()?; let project_id = if project_only {
if !self.leader_state.followers.is_empty() { self.project.read(cx).remote_id()
self.app_state } else {
.client
.send(proto::UpdateFollowers {
project_id,
follower_ids: self.leader_state.followers.iter().copied().collect(),
variant: Some(update),
})
.log_err();
}
None 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> { pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
@ -3081,32 +3088,40 @@ impl Workspace {
let room = call.read(cx).room()?.read(cx); let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?; let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_activate = Vec::new(); let mut items_to_activate = Vec::new();
let leader_in_this_app;
let leader_in_this_project;
match participant.location { match participant.location {
call::ParticipantLocation::SharedProject { project_id } => { call::ParticipantLocation::SharedProject { project_id } => {
if Some(project_id) == self.project.read(cx).remote_id() { leader_in_this_app = true;
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { leader_in_this_project = Some(project_id) == self.project.read(cx).remote_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)));
} }
call::ParticipantLocation::UnsharedProject => {
leader_in_this_app = true;
leader_in_this_project = false;
} }
}
}
call::ParticipantLocation::UnsharedProject => {}
call::ParticipantLocation::External => { call::ParticipantLocation::External => {
for (pane, _) in self.follower_states_by_leader.get(&leader_id)? { 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) { if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen))); items_to_activate.push((pane.clone(), Box::new(shared_screen)));
} }
} }
}
}
for (pane, item) in items_to_activate { for (pane, item) in items_to_activate {
let pane_was_focused = pane.read(cx).has_focus(); let pane_was_focused = pane.read(cx).has_focus();
@ -3149,6 +3164,7 @@ impl Workspace {
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) { pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active { if active {
self.update_active_view_for_followers(cx);
cx.background() cx.background()
.spawn(persistence::DB.update_timestamp(self.database_id())) .spawn(persistence::DB.update_timestamp(self.database_id()))
.detach(); .detach();
@ -3522,8 +3538,10 @@ impl Workspace {
let channel_store = let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); 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 { let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(), languages: project.read(cx).languages().clone(),
workspace_store,
client, client,
user_store, user_store,
channel_store, channel_store,
@ -3767,6 +3785,12 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
impl Entity for Workspace { impl Entity for Workspace {
type Event = Event; 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 { 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 { impl ViewId {
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> { pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
Ok(Self { Ok(Self {
@ -4077,21 +4246,20 @@ pub fn join_remote_project(
cx: &mut AppContext, cx: &mut AppContext,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let existing_workspace = cx let windows = cx.windows();
.windows() let existing_workspace = windows.into_iter().find_map(|window| {
.into_iter()
.find_map(|window| {
window.downcast::<Workspace>().and_then(|window| { window.downcast::<Workspace>().and_then(|window| {
window.read_root_with(&cx, |workspace, cx| { window
.read_root_with(&cx, |workspace, cx| {
if workspace.project().read(cx).remote_id() == Some(project_id) { if workspace.project().read(cx).remote_id() == Some(project_id) {
Some(cx.handle().downgrade()) Some(cx.handle().downgrade())
} else { } else {
None None
} }
}) })
.unwrap_or(None)
}) })
}) });
.flatten();
let workspace = if let Some(existing_workspace) = existing_workspace { let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace existing_workspace
@ -4156,13 +4324,11 @@ pub fn join_remote_project(
}); });
if let Some(follow_peer_id) = follow_peer_id { if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace workspace
.toggle_follow(follow_peer_id, cx) .follow(follow_peer_id, cx)
.map(|follow| follow.detach_and_log_err(cx)); .map(|follow| follow.detach_and_log_err(cx));
} }
} }
}
})?; })?;
anyhow::Ok(()) anyhow::Ok(())

View file

@ -6,12 +6,11 @@ use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc}; use std::{borrow::Cow, str, sync::Arc};
use util::asset_str; use util::asset_str;
use self::elixir_next::ElixirSettings; use self::elixir::ElixirSettings;
mod c; mod c;
mod css; mod css;
mod elixir; mod elixir;
mod elixir_next;
mod go; mod go;
mod html; mod html;
mod json; mod json;
@ -46,7 +45,7 @@ pub fn init(
node_runtime: Arc<dyn NodeRuntime>, node_runtime: Arc<dyn NodeRuntime>,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
settings::register::<elixir_next::ElixirSettings>(cx); settings::register::<elixir::ElixirSettings>(cx);
let language = |name, grammar, adapters| { let language = |name, grammar, adapters| {
languages.register(name, load_config(name), grammar, adapters, load_queries) languages.register(name, load_config(name), grammar, adapters, load_queries)
@ -72,21 +71,21 @@ pub fn init(
], ],
); );
match &settings::get::<ElixirSettings>(cx).next { match &settings::get::<ElixirSettings>(cx).lsp {
elixir_next::ElixirNextSetting::Off => language( elixir::ElixirLspSetting::ElixirLs => language(
"elixir", "elixir",
tree_sitter_elixir::language(), tree_sitter_elixir::language(),
vec![Arc::new(elixir::ElixirLspAdapter)], vec![Arc::new(elixir::ElixirLspAdapter)],
), ),
elixir_next::ElixirNextSetting::On => language( elixir::ElixirLspSetting::NextLs => language(
"elixir", "elixir",
tree_sitter_elixir::language(), 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", "elixir",
tree_sitter_elixir::language(), tree_sitter_elixir::language(),
vec![Arc::new(elixir_next::LocalNextLspAdapter { vec![Arc::new(elixir::LocalLspAdapter {
path: path.clone(), path: path.clone(),
arguments: arguments.clone(), arguments: arguments.clone(),
})], })],

View file

@ -1,12 +1,17 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use gpui::{AsyncAppContext, Task}; use gpui::{AsyncAppContext, Task};
pub use language::*; pub use language::*;
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
use smol::fs::{self, File}; use smol::fs::{self, File};
use std::{ use std::{
any::Any, any::Any,
env::consts,
ops::Deref,
path::PathBuf, path::PathBuf,
sync::{ sync::{
atomic::{AtomicBool, Ordering::SeqCst}, atomic::{AtomicBool, Ordering::SeqCst},
@ -14,11 +19,50 @@ use std::{
}, },
}; };
use util::{ use util::{
async_iife,
fs::remove_matching, fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion}, github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt, 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; pub struct ElixirLspAdapter;
#[async_trait] #[async_trait]
@ -144,14 +188,14 @@ impl LspAdapter for ElixirLspAdapter {
container_dir: PathBuf, container_dir: PathBuf,
_: &dyn LspAdapterDelegate, _: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> { ) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await get_cached_server_binary_elixir_ls(container_dir).await
} }
async fn installation_test_binary( async fn installation_test_binary(
&self, &self,
container_dir: PathBuf, container_dir: PathBuf,
) -> Option<LanguageServerBinary> { ) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await get_cached_server_binary_elixir_ls(container_dir).await
} }
async fn label_for_completion( 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 { (|| async move {
let mut last = None; let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?; 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 .await
.log_err() .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(),
})
}

View file

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

View file

@ -2,6 +2,7 @@
[(line_comment) (attribute_item)]* @context [(line_comment) (attribute_item)]* @context
. .
[ [
(struct_item (struct_item
name: (_) @name) name: (_) @name)
@ -26,3 +27,6 @@
name: (_) @name) name: (_) @name)
] @item ] @item
) )
(attribute_item) @collapse
(use_declaration) @collapse

View file

@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
use fs::RealFs; use fs::RealFs;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::AppState; use workspace::{AppState, WorkspaceStore};
use zed::{ use zed::{
assets::Assets, assets::Assets,
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, 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 user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_store = let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); 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()); cx.set_global(client.clone());
@ -176,7 +177,7 @@ fn main() {
}) })
.detach(); .detach();
client.telemetry().start(installation_id); client.telemetry().start(installation_id, cx);
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
languages, languages,
@ -187,6 +188,7 @@ fn main() {
build_window_options, build_window_options,
initialize_workspace, initialize_workspace,
background_actions, background_actions,
workspace_store,
}); });
cx.set_global(Arc::downgrade(&app_state)); cx.set_global(Arc::downgrade(&app_state));

View file

@ -44,6 +44,7 @@ position_2=${half_width},${y}
# Authenticate using the collab server's admin secret. # Authenticate using the collab server's admin secret.
export ZED_STATELESS=1 export ZED_STATELESS=1
export ZED_ALWAYS_ACTIVE=1
export ZED_ADMIN_API_TOKEN=secret export ZED_ADMIN_API_TOKEN=secret
export ZED_SERVER_URL=http://localhost:8080 export ZED_SERVER_URL=http://localhost:8080
export ZED_WINDOW_SIZE=${half_width},${height} export ZED_WINDOW_SIZE=${half_width},${height}

View file

@ -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 { interactive, toggleable } from "../element"
import { useTheme, with_opacity } from "../theme" import { useTheme, with_opacity } from "../theme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
@ -191,6 +191,12 @@ export function titlebar(): any {
color: "variant", color: "variant",
}), }),
project_host: text_button({
text_properties: {
weight: "bold"
}
}),
// Collaborators // Collaborators
leader_avatar: { leader_avatar: {
width: avatar_width, width: avatar_width,