Detailed changes
@@ -1082,7 +1082,6 @@ dependencies = [
"anyhow",
"async-broadcast",
"audio",
- "channel",
"client",
"collections",
"fs",
@@ -1467,7 +1466,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.23.1"
+version = "0.23.3"
dependencies = [
"anyhow",
"async-trait",
@@ -2079,9 +2078,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.66+curl-8.3.0"
+version = "0.4.67+curl-8.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
+checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34"
dependencies = [
"cc",
"libc",
@@ -2832,7 +2831,6 @@ dependencies = [
"parking_lot 0.11.2",
"regex",
"rope",
- "rpc",
"serde",
"serde_derive",
"serde_json",
@@ -9665,6 +9663,7 @@ dependencies = [
"theme",
"theme_selector",
"util",
+ "vim",
"workspace",
]
@@ -9971,7 +9970,6 @@ dependencies = [
"async-recursion 1.0.5",
"bincode",
"call",
- "channel",
"client",
"collections",
"context_menu",
@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
-FROM rust:1.72-bullseye as builder
+FROM rust:1.73-bullseye as builder
WORKDIR app
COPY . .
@@ -83,9 +83,7 @@ foreman start
If you want to run Zed pointed at the local servers, you can run:
```
-script/zed-with-local-servers
-# or...
-script/zed-with-local-servers --release
+script/zed-local
```
### Dump element JSON
@@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
-channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
@@ -5,7 +5,6 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
-use channel::ChannelId;
use client::{
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
@@ -79,7 +78,7 @@ impl ActiveCall {
}
}
- pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
+ pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}
@@ -606,35 +606,39 @@ impl Room {
/// Returns the most 'active' projects, defined as most people in the project
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
- let mut projects = HashMap::default();
- let mut hosts = HashMap::default();
-
+ let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
for participant in self.remote_participants.values() {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
- *projects.entry(project_id).or_insert(0) += 1;
+ project_hosts_and_guest_counts
+ .entry(project_id)
+ .or_default()
+ .1 += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
- *projects.entry(project.id).or_insert(0) += 1;
- hosts.insert(project.id, participant.user.id);
+ project_hosts_and_guest_counts
+ .entry(project.id)
+ .or_default()
+ .0 = Some(participant.user.id);
}
}
if let Some(user) = self.user_store.read(cx).current_user() {
for project in &self.local_participant.projects {
- *projects.entry(project.id).or_insert(0) += 1;
- hosts.insert(project.id, user.id);
+ project_hosts_and_guest_counts
+ .entry(project.id)
+ .or_default()
+ .0 = Some(user.id);
}
}
- let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
- pairs.sort_by_key(|(_, count)| *count as i32);
-
- pairs
- .iter()
- .find_map(|(project_id, _)| hosts.get(project_id).map(|host| (*project_id, *host)))
+ project_hosts_and_guest_counts
+ .into_iter()
+ .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
+ .max_by_key(|(_, _, guest_count)| *guest_count)
+ .map(|(id, host, _)| (id, host))
}
async fn handle_room_updated(
@@ -700,6 +704,7 @@ impl Room {
let Some(peer_id) = participant.peer_id else {
continue;
};
+ let participant_index = ParticipantIndex(participant.participant_index);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
@@ -750,8 +755,9 @@ impl Room {
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
- remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
+ remote_participant.projects = participant.projects;
+ remote_participant.participant_index = participant_index;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -763,9 +769,7 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
- participant_index: ParticipantIndex(
- participant.participant_index,
- ),
+ participant_index,
peer_id,
projects: participant.projects,
location,
@@ -2,19 +2,21 @@ mod channel_buffer;
mod channel_chat;
mod channel_store;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use std::sync::Arc;
+
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
pub use channel_store::{
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
-use client::Client;
-use std::sync::Arc;
-
#[cfg(test)]
mod channel_store_tests;
-pub fn init(client: &Arc<Client>) {
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+ channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}
@@ -2,6 +2,7 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{anyhow, Result};
+use channel_index::ChannelIndex;
use client::{Client, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
@@ -15,7 +16,11 @@ use serde_derive::{Deserialize, Serialize};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt;
-use self::channel_index::ChannelIndex;
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+ let channel_store =
+ cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ cx.set_global(channel_store);
+}
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -92,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
}
impl ChannelStore {
+ pub fn global(cx: &AppContext) -> ModelHandle<Self> {
+ cx.global::<ModelHandle<Self>>().clone()
+ }
+
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
- crate::init(&client);
client::init(&client, cx);
+ crate::init(&client, user_store, cx);
- cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
+ ChannelStore::global(cx)
}
fn update_channels(
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
-actions!(client, [SignIn, SignOut]);
+actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
}
}
});
+ cx.add_global_action({
+ let client = client.clone();
+ move |_: &Reconnect, cx| {
+ if let Some(client) = client.upgrade() {
+ cx.spawn(|cx| async move {
+ client.reconnect(&cx);
+ })
+ .detach();
+ }
+ }
+ });
}
pub struct Client {
@@ -1212,6 +1223,11 @@ impl Client {
self.set_status(Status::SignedOut, cx);
}
+ pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+ self.peer.teardown();
+ self.set_status(Status::ConnectionLost, cx);
+ }
+
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
@@ -8,7 +8,6 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
-use uuid::Uuid;
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
@@ -20,7 +19,7 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
- session_id: String, // Per app launch
+ session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
@@ -43,7 +42,7 @@ lazy_static! {
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
- session_id: String,
+ session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
os_name: &'static str,
@@ -134,7 +133,7 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
- session_id: Uuid::new_v4().to_string(),
+ session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
@@ -149,9 +148,15 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
- pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
+ pub fn start(
+ self: &Arc<Self>,
+ installation_id: Option<String>,
+ session_id: String,
+ cx: &mut AppContext,
+ ) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
+ state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
@@ -283,23 +288,21 @@ impl Telemetry {
{
let state = this.state.lock();
+ let request_body = ClickhouseEventRequestBody {
+ token: ZED_SECRET_CLIENT_TOKEN,
+ installation_id: state.installation_id.clone(),
+ session_id: state.session_id.clone(),
+ is_staff: state.is_staff.clone(),
+ app_version: state.app_version.clone(),
+ os_name: state.os_name,
+ os_version: state.os_version.clone(),
+ architecture: state.architecture,
+
+ release_channel: state.release_channel,
+ events,
+ };
json_bytes.clear();
- serde_json::to_writer(
- &mut json_bytes,
- &ClickhouseEventRequestBody {
- token: ZED_SECRET_CLIENT_TOKEN,
- installation_id: state.installation_id.clone(),
- session_id: state.session_id.clone(),
- is_staff: state.is_staff.clone(),
- app_version: state.app_version.clone(),
- os_name: state.os_name,
- os_version: state.os_version.clone(),
- architecture: state.architecture,
-
- release_channel: state.release_channel,
- events,
- },
- )?;
+ serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.23.1"
+version = "0.23.3"
publish = false
[[bin]]
@@ -89,7 +89,7 @@ impl Database {
let mut rows = channel_message::Entity::find()
.filter(condition)
- .order_by_asc(channel_message::Column::Id)
+ .order_by_desc(channel_message::Column::Id)
.limit(count as u64)
.stream(&*tx)
.await?;
@@ -110,6 +110,7 @@ impl Database {
});
}
drop(rows);
+ messages.reverse();
Ok(messages)
})
.await
@@ -1,10 +1,75 @@
use crate::{
- db::{Database, NewUserParams},
+ db::{Database, MessageId, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
use time::OffsetDateTime;
+test_both_dbs!(
+ test_channel_message_retrieval,
+ test_channel_message_retrieval_postgres,
+ test_channel_message_retrieval_sqlite
+);
+
+async fn test_channel_message_retrieval(db: &Arc<Database>) {
+ let user = db
+ .create_user(
+ "user@example.com",
+ false,
+ NewUserParams {
+ github_login: "user".into(),
+ github_user_id: 1,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ let channel = db
+ .create_channel("channel", None, "room", user)
+ .await
+ .unwrap();
+
+ let owner_id = db.create_server("test").await.unwrap().0 as u32;
+ db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
+ .await
+ .unwrap();
+
+ let mut all_messages = Vec::new();
+ for i in 0..10 {
+ all_messages.push(
+ db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
+ .await
+ .unwrap()
+ .0
+ .to_proto(),
+ );
+ }
+
+ let messages = db
+ .get_channel_messages(channel, user, 3, None)
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|message| message.id)
+ .collect::<Vec<_>>();
+ assert_eq!(messages, &all_messages[7..10]);
+
+ let messages = db
+ .get_channel_messages(
+ channel,
+ user,
+ 4,
+ Some(MessageId::from_proto(all_messages[6])),
+ )
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|message| message.id)
+ .collect::<Vec<_>>();
+ assert_eq!(messages, &all_messages[2..6]);
+}
+
test_both_dbs!(
test_channel_message_nonces,
test_channel_message_nonces_postgres,
@@ -1917,13 +1917,10 @@ async fn follow(
.check_room_participants(room_id, leader_id, session.connection_id)
.await?;
- let mut response_payload = session
+ let response_payload = session
.peer
.forward_request(session.connection_id, leader_id, request)
.await?;
- response_payload
- .views
- .retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
if let Some(project_id) = project_id {
@@ -1984,14 +1981,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
.await?
};
- let leader_id = request.variant.as_ref().and_then(|variant| match variant {
- proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+ // For now, don't send view update messages back to that view's current leader.
+ let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
- proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+ _ => None,
});
+
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
- if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
+ if Some(follower_peer_id) != connection_id_to_omit
+ && connection_ids.contains(&follower_connection_id)
+ {
session.peer.forward_send(
session.connection_id,
follower_connection_id,
@@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification;
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
use live_kit_client::MacOSDisplay;
+use rpc::proto::PeerId;
use serde_json::json;
use std::{borrow::Cow, sync::Arc};
use workspace::{
@@ -183,20 +184,12 @@ async fn test_basic_following(
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
}
// Client C unfollows client A.
@@ -206,46 +199,39 @@ async fn test_basic_following(
// All clients see that clients B is following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b])],
+ "followers seen by {name}"
+ );
}
// Client C re-follows client A.
- workspace_c.update(cx_c, |workspace, cx| {
- workspace.follow(peer_id_a, cx);
- });
+ workspace_c
+ .update(cx_c, |workspace, cx| {
+ workspace.follow(peer_id_a, cx).unwrap()
+ })
+ .await
+ .unwrap();
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
}
- // Client D follows client C.
+ // Client D follows client B, then switches to following client C.
+ workspace_d
+ .update(cx_d, |workspace, cx| {
+ workspace.follow(peer_id_b, cx).unwrap()
+ })
+ .await
+ .unwrap();
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_c, cx).unwrap()
@@ -255,20 +241,15 @@ async fn test_basic_following(
// All clients see that D is following C
cx_d.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[peer_id_d],
- "checking followers for C as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[
+ (peer_id_a, vec![peer_id_b, peer_id_c]),
+ (peer_id_c, vec![peer_id_d])
+ ],
+ "followers seen by {name}"
+ );
}
// Client C closes the project.
@@ -277,32 +258,12 @@ async fn test_basic_following(
// Clients A and B see that client B is following A, and client C is not present in the followers.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
- }
-
- // All clients see that no-one is following C
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[],
- "checking followers for C as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b]),],
+ "followers seen by {name}"
+ );
}
// When client A activates a different editor, client B does so as well.
@@ -724,10 +685,9 @@ async fn test_peers_following_each_other(
.await
.unwrap();
- // Client A opens some editors.
+ // Client A opens a file.
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
- let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
- let _editor_a1 = workspace_a
+ workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
@@ -736,10 +696,9 @@ async fn test_peers_following_each_other(
.downcast::<Editor>()
.unwrap();
- // Client B opens an editor.
+ // Client B opens a different file.
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
- let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
- let _editor_b1 = workspace_b
+ workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
})
@@ -754,9 +713,7 @@ async fn test_peers_following_each_other(
});
workspace_a
.update(cx_a, |workspace, cx| {
- assert_ne!(*workspace.active_pane(), pane_a1);
- let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
- workspace.follow(leader_id, cx).unwrap()
+ workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
})
.await
.unwrap();
@@ -765,85 +722,443 @@ async fn test_peers_following_each_other(
});
workspace_b
.update(cx_b, |workspace, cx| {
- assert_ne!(*workspace.active_pane(), pane_b1);
- let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
- workspace.follow(leader_id, cx).unwrap()
+ workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
})
.await
.unwrap();
- workspace_a.update(cx_a, |workspace, cx| {
- workspace.activate_next_pane(cx);
- });
- // Wait for focus effects to be fully flushed
- workspace_a.update(cx_a, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_a1);
- });
+ // Clients A and B return focus to the original files they had open
+ workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ deterministic.run_until_parked();
+ // Both clients see the other client's focused file in their right pane.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "1.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_b.peer_id(),
+ items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "2.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
+ },
+ ]
+ );
+
+ // Clients A and B each open a new file.
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
})
.await
.unwrap();
- workspace_b.update(cx_b, |workspace, cx| {
- workspace.activate_next_pane(cx);
- });
workspace_b
.update(cx_b, |workspace, cx| {
- assert_eq!(*workspace.active_pane(), pane_b1);
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
})
.await
.unwrap();
- cx_a.foreground().run_until_parked();
+ deterministic.run_until_parked();
- // Ensure leader updates don't change the active pane of followers
- workspace_a.read_with(cx_a, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_a1);
+ // Both client's see the other client open the new file, but keep their
+ // focus on their own active pane.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (true, "3.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client A focuses their right pane, in which they're following client B.
+ workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+ deterministic.run_until_parked();
+
+ // Client B sees that client A is now looking at the same file as them.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: false,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (false, "3.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client B focuses their right pane, in which they're following client A,
+ // who is following them.
+ workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+ deterministic.run_until_parked();
+
+ // Client A sees that client B is now looking at the same file as them.
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (false, "3.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into())
+ ]
+ },
+ ]
+ );
+
+ // Client B focuses a file that they previously followed A to, breaking
+ // the follow.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
});
- workspace_b.read_with(cx_b, |workspace, _| {
- assert_eq!(*workspace.active_pane(), pane_b1);
+ deterministic.run_until_parked();
+
+ // Both clients see that client B is looking at that previous file.
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "2.txt".into()),
+ (false, "1.txt".into()),
+ (true, "3.txt".into()),
+ (false, "4.txt".into())
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (true, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+
+ // Client B closes tabs, some of which were originally opened by client A,
+ // and some of which were originally opened by client B.
+ workspace_b.update(cx_b, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.close_inactive_items(&Default::default(), cx)
+ .unwrap()
+ .detach();
+ });
});
- // Ensure peers following each other doesn't cause an infinite loop.
+ deterministic.run_until_parked();
+
+ // Both clients see that Client B is looking at the previous tab.
assert_eq!(
- workspace_a.read_with(cx_a, |workspace, cx| workspace
- .active_item(cx)
- .unwrap()
- .project_path(cx)),
- Some((worktree_id, "3.txt").into())
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![(true, "3.txt".into()),]
+ },
+ ]
);
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_b.peer_id(),
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (true, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+
+ // Client B follows client A again.
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ // Client A cycles through some tabs.
workspace_a.update(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "3.txt").into())
- );
- workspace.activate_next_pane(cx);
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
});
+ deterministic.run_until_parked();
+
+ // Client B follows client A into those tabs.
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "1.txt".into()),
+ (false, "2.txt".into()),
+ (true, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
+ },
+ ]
+ );
workspace_a.update(cx_a, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "4.txt").into())
- );
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
});
+ deterministic.run_until_parked();
- workspace_b.update(cx_b, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "4.txt").into())
- );
- workspace.activate_next_pane(cx);
- });
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (false, "1.txt".into()),
+ (true, "2.txt".into()),
+ (false, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "3.txt".into()),
+ (false, "4.txt".into()),
+ (true, "2.txt".into())
+ ]
+ },
+ ]
+ );
- workspace_b.update(cx_b, |workspace, cx| {
- assert_eq!(
- workspace.active_item(cx).unwrap().project_path(cx),
- Some((worktree_id, "3.txt").into())
- );
+ workspace_a.update(cx_a, |workspace, cx| {
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.activate_prev_item(true, cx);
+ });
});
+ deterministic.run_until_parked();
+
+ assert_eq!(
+ pane_summaries(&workspace_a, cx_a),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: None,
+ items: vec![
+ (true, "1.txt".into()),
+ (false, "2.txt".into()),
+ (false, "4.txt".into()),
+ (false, "3.txt".into()),
+ ]
+ },
+ ]
+ );
+ assert_eq!(
+ pane_summaries(&workspace_b, cx_b),
+ &[
+ PaneSummary {
+ active: false,
+ leader: None,
+ items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+ },
+ PaneSummary {
+ active: true,
+ leader: client_a.peer_id(),
+ items: vec![
+ (false, "3.txt".into()),
+ (false, "4.txt".into()),
+ (false, "2.txt".into()),
+ (true, "1.txt".into()),
+ ]
+ },
+ ]
+ );
}
#[gpui::test(iterations = 10)]
@@ -1074,24 +1389,6 @@ async fn test_peers_simultaneously_following_each_other(
});
}
-fn visible_push_notifications(
- cx: &mut TestAppContext,
-) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
- let mut ret = Vec::new();
- for window in cx.windows() {
- window.read_with(cx, |window| {
- if let Some(handle) = window
- .root_view()
- .clone()
- .downcast::<ProjectSharedNotification>()
- {
- ret.push(handle)
- }
- });
- }
- ret
-}
-
#[gpui::test(iterations = 10)]
async fn test_following_across_workspaces(
deterministic: Arc<Deterministic>,
@@ -1304,3 +1601,83 @@ async fn test_following_across_workspaces(
assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
});
}
+
+fn visible_push_notifications(
+ cx: &mut TestAppContext,
+) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+ let mut ret = Vec::new();
+ for window in cx.windows() {
+ window.read_with(cx, |window| {
+ if let Some(handle) = window
+ .root_view()
+ .clone()
+ .downcast::<ProjectSharedNotification>()
+ {
+ ret.push(handle)
+ }
+ });
+ }
+ ret
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+ active: bool,
+ leader: Option<PeerId>,
+ items: Vec<(bool, String)>,
+}
+
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+ cx.read(|cx| {
+ let active_call = ActiveCall::global(cx).read(cx);
+ let peer_id = active_call.client().peer_id();
+ let room = active_call.room().unwrap().read(cx);
+ let mut result = room
+ .remote_participants()
+ .values()
+ .map(|participant| participant.peer_id)
+ .chain(peer_id)
+ .filter_map(|peer_id| {
+ let followers = room.followers_for(peer_id, project_id);
+ if followers.is_empty() {
+ None
+ } else {
+ Some((peer_id, followers.to_vec()))
+ }
+ })
+ .collect::<Vec<_>>();
+ result.sort_by_key(|e| e.0);
+ result
+ })
+}
+
+fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
+ workspace.read_with(cx, |workspace, cx| {
+ let active_pane = workspace.active_pane();
+ workspace
+ .panes()
+ .iter()
+ .map(|pane| {
+ let leader = workspace.leader_for_pane(pane);
+ let active = pane == active_pane;
+ let pane = pane.read(cx);
+ let active_ix = pane.active_item_index();
+ PaneSummary {
+ active,
+ leader,
+ items: pane
+ .items()
+ .enumerate()
+ .map(|(ix, item)| {
+ (
+ ix == active_ix,
+ item.tab_description(0, cx)
+ .map_or(String::new(), |s| s.to_string()),
+ )
+ })
+ .collect(),
+ }
+ })
+ .collect()
+ })
+}
@@ -44,6 +44,7 @@ pub struct TestServer {
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
+ channel_store: ModelHandle<ChannelStore>,
state: RefCell<TestClientState>,
}
@@ -206,15 +207,12 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
- channel_store: channel_store.clone(),
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
@@ -231,7 +229,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
- channel::init(&client);
+ channel::init(&client, user_store, cx);
});
client
@@ -242,6 +240,7 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
+ channel_store: cx.read(ChannelStore::global).clone(),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -310,10 +309,9 @@ impl TestServer {
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
- let (admin_client, admin_cx) = admin;
- let channel_id = admin_client
- .app_state
- .channel_store
+ let (_, admin_cx) = admin;
+ let channel_id = admin_cx
+ .read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
@@ -321,9 +319,8 @@ impl TestServer {
.unwrap();
for (member_client, member_cx) in members {
- admin_client
- .app_state
- .channel_store
+ admin_cx
+ .read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
@@ -337,9 +334,8 @@ impl TestServer {
admin_cx.foreground().run_until_parked();
- member_client
- .app_state
- .channel_store
+ member_cx
+ .read(ChannelStore::global)
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
@@ -447,7 +443,7 @@ impl TestClient {
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
- &self.app_state.channel_store
+ &self.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -614,8 +610,8 @@ impl TestClient {
) {
let (other_client, other_cx) = user;
- self.app_state
- .channel_store
+ cx_self
+ .read(ChannelStore::global)
.update(cx_self, |channel_store, cx| {
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
})
@@ -624,11 +620,10 @@ impl TestClient {
cx_self.foreground().run_until_parked();
- other_client
- .app_state
- .channel_store
- .update(other_cx, |channels, _| {
- channels.respond_to_channel_invite(channel, true)
+ other_cx
+ .read(ChannelStore::global)
+ .update(other_cx, |channel_store, _| {
+ channel_store.respond_to_channel_invite(channel, true)
})
.await
.unwrap();
@@ -73,7 +73,7 @@ impl ChannelView {
) -> Task<Result<ViewHandle<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
- let channel_store = workspace.app_state().channel_store.clone();
+ let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
@@ -81,7 +81,7 @@ impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
- let channel_store = workspace.app_state().channel_store.clone();
+ let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
@@ -655,7 +655,7 @@ impl CollabPanel {
channel_editing_state: None,
selection: None,
user_store: workspace.user_store().clone(),
- channel_store: workspace.app_state().channel_store.clone(),
+ channel_store: ChannelStore::global(cx),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -107,13 +107,23 @@ fn matching_history_item_paths(
) -> HashMap<Arc<Path>, PathMatch> {
let history_items_by_worktrees = history_items
.iter()
- .map(|found_path| {
- let path = &found_path.project.path;
+ .filter_map(|found_path| {
let candidate = PathMatchCandidate {
- path,
- char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
+ path: &found_path.project.path,
+ // Only match history items names, otherwise their paths may match too many queries, producing false positives.
+ // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
+ // it would be shown first always, despite the latter being a better match.
+ char_bag: CharBag::from_iter(
+ found_path
+ .project
+ .path
+ .file_name()?
+ .to_string_lossy()
+ .to_lowercase()
+ .chars(),
+ ),
};
- (found_path.project.worktree_id, candidate)
+ Some((found_path.project.worktree_id, candidate))
})
.fold(
HashMap::default(),
@@ -1803,6 +1813,113 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_history_items_vs_very_good_external_match(
+ deterministic: Arc<gpui::executor::Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "collab_ui": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ "collab_ui.rs": "// Fourth Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+ // generate some history to select from
+ open_close_queried_buffer(
+ "fir",
+ 1,
+ "first.rs",
+ window.into(),
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ open_close_queried_buffer(
+ "sec",
+ 1,
+ "second.rs",
+ window.into(),
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ open_close_queried_buffer(
+ "thi",
+ 1,
+ "third.rs",
+ window.into(),
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+ open_close_queried_buffer(
+ "sec",
+ 1,
+ "second.rs",
+ window.into(),
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+
+ cx.dispatch_action(window.into(), Toggle);
+ let query = "collab_ui";
+ let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+ finder
+ .update(cx, |finder, cx| {
+ finder.delegate_mut().update_matches(query.to_string(), cx)
+ })
+ .await;
+ finder.read_with(cx, |finder, _| {
+ let delegate = finder.delegate();
+ assert!(
+ delegate.matches.history.is_empty(),
+ "History items should not math query {query}, they should be matched by name only"
+ );
+
+ let search_entries = delegate
+ .matches
+ .search
+ .iter()
+ .map(|e| e.path.to_path_buf())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ search_entries.len(),
+ 4,
+ "All history and the new file should be found after query {query} as search results"
+ );
+ assert_eq!(
+ search_entries,
+ vec![
+ PathBuf::from("collab_ui/collab_ui.rs"),
+ PathBuf::from("collab_ui/third.rs"),
+ PathBuf::from("collab_ui/first.rs"),
+ PathBuf::from("collab_ui/second.rs"),
+ ],
+ "Despite all search results having the same directory name, the most matching one should be on top"
+ );
+ });
+ }
+
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,
@@ -13,7 +13,6 @@ rope = { path = "../rope" }
text = { path = "../text" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
-rpc = { path = "../rpc" }
anyhow.workspace = true
async-trait.workspace = true
@@ -2,7 +2,6 @@ use anyhow::Result;
use collections::HashMap;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
-use rpc::proto;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -23,6 +22,7 @@ pub struct Branch {
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
+
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -358,24 +358,6 @@ impl GitFileStatus {
}
}
}
-
- pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
- git_status.and_then(|status| {
- proto::GitStatus::from_i32(status).map(|status| match status {
- proto::GitStatus::Added => GitFileStatus::Added,
- proto::GitStatus::Modified => GitFileStatus::Modified,
- proto::GitStatus::Conflict => GitFileStatus::Conflict,
- })
- })
- }
-
- pub fn to_proto(self) -> i32 {
- match self {
- GitFileStatus::Added => proto::GitStatus::Added as i32,
- GitFileStatus::Modified => proto::GitStatus::Modified as i32,
- GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
- }
- }
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
@@ -441,7 +441,7 @@ mod tests {
score,
worktree_id: 0,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},
@@ -14,7 +14,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
- pub path: &'a Arc<Path>,
+ pub path: &'a Path,
pub char_bag: CharBag,
}
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
score,
worktree_id,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
score,
worktree_id,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,
@@ -71,7 +71,7 @@ pub struct Window {
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
- text_layout_cache: TextLayoutCache,
+ text_layout_cache: Arc<TextLayoutCache>,
refreshing: bool,
}
@@ -107,7 +107,7 @@ impl Window {
cursor_regions: Default::default(),
mouse_regions: Default::default(),
event_handlers: Default::default(),
- text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
+ text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())),
last_mouse_moved_event: None,
last_mouse_position: Vector2F::zero(),
pressed_buttons: Default::default(),
@@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> {
self.window.refreshing
}
- pub fn text_layout_cache(&self) -> &TextLayoutCache {
+ pub fn text_layout_cache(&self) -> &Arc<TextLayoutCache> {
&self.window.text_layout_cache
}
@@ -5,7 +5,7 @@ use crate::{
use anyhow::Result;
use gpui::{
geometry::{vector::Vector2F, Size},
- text_layout::LineLayout,
+ text_layout::Line,
LayoutId,
};
use parking_lot::Mutex;
@@ -32,7 +32,7 @@ impl<V: 'static> Element<V> for Text {
_view: &mut V,
cx: &mut ViewContext<V>,
) -> Result<(LayoutId, Self::PaintState)> {
- let fonts = cx.platform().fonts();
+ let layout_cache = cx.text_layout_cache().clone();
let text_style = cx.text_style();
let line_height = cx.font_cache().line_height(text_style.font_size);
let text = self.text.clone();
@@ -41,14 +41,14 @@ impl<V: 'static> Element<V> for Text {
let layout_id = cx.add_measured_layout_node(Default::default(), {
let paint_state = paint_state.clone();
move |_params| {
- let line_layout = fonts.layout_line(
+ let line_layout = layout_cache.layout_str(
text.as_ref(),
text_style.font_size,
&[(text.len(), text_style.to_run())],
);
let size = Size {
- width: line_layout.width,
+ width: line_layout.width(),
height: line_height,
};
@@ -85,13 +85,9 @@ impl<V: 'static> Element<V> for Text {
line_height = paint_state.line_height;
}
- let text_style = cx.text_style();
- let line =
- gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
-
// TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
let visible_bounds = bounds;
- line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
+ line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
}
}
@@ -104,6 +100,6 @@ impl<V: 'static> IntoElement<V> for Text {
}
pub struct TextLayout {
- line_layout: Arc<LineLayout>,
+ line_layout: Arc<Line>,
line_height: f32,
}
@@ -22,7 +22,6 @@ test-support = [
]
[dependencies]
-client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
@@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
// Insert the block at column zero. The entire block is indented
// so that the first line matches the previous line's indentation.
buffer.edit(
- [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+ [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
@@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
- git_status: entry.git_status.map(|status| status.to_proto()),
+ git_status: entry.git_status.map(git_status_to_proto),
}
}
}
@@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
- git_status: GitFileStatus::from_proto(entry.git_status),
+ git_status: git_status_from_proto(entry.git_status),
})
} else {
Err(anyhow!(
@@ -4366,3 +4366,21 @@ fn combine_git_statuses(
unstaged
}
}
+
+fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
+ git_status.and_then(|status| {
+ proto::GitStatus::from_i32(status).map(|status| match status {
+ proto::GitStatus::Added => GitFileStatus::Added,
+ proto::GitStatus::Modified => GitFileStatus::Modified,
+ proto::GitStatus::Conflict => GitFileStatus::Conflict,
+ })
+ })
+}
+
+fn git_status_to_proto(status: GitFileStatus) -> i32 {
+ match status {
+ GitFileStatus::Added => proto::GitStatus::Added as i32,
+ GitFileStatus::Modified => proto::GitStatus::Modified as i32,
+ GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
+ }
+}
@@ -6,13 +6,17 @@ pub mod collab_panel;
pub mod context_menu;
pub mod facepile;
pub mod keybinding;
+pub mod language_selector;
+pub mod multi_buffer;
pub mod palette;
pub mod panel;
pub mod project_panel;
+pub mod recent_projects;
pub mod status_bar;
pub mod tab;
pub mod tab_bar;
pub mod terminal;
+pub mod theme_selector;
pub mod title_bar;
pub mod toolbar;
pub mod traffic_lights;
@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::LanguageSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct LanguageSelectorStory {}
+
+impl LanguageSelectorStory {
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ Story::container(cx)
+ .child(Story::title_for::<_, LanguageSelector>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(LanguageSelector::new())
+ }
+}
@@ -0,0 +1,24 @@
+use ui::prelude::*;
+use ui::{hello_world_rust_buffer_example, MultiBuffer};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct MultiBufferStory {}
+
+impl MultiBufferStory {
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx);
+
+ Story::container(cx)
+ .child(Story::title_for::<_, MultiBuffer<V>>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(MultiBuffer::new(vec![
+ hello_world_rust_buffer_example(&theme),
+ hello_world_rust_buffer_example(&theme),
+ hello_world_rust_buffer_example(&theme),
+ hello_world_rust_buffer_example(&theme),
+ hello_world_rust_buffer_example(&theme),
+ ]))
+ }
+}
@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::RecentProjects;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct RecentProjectsStory {}
+
+impl RecentProjectsStory {
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ Story::container(cx)
+ .child(Story::title_for::<_, RecentProjects>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(RecentProjects::new())
+ }
+}
@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::ThemeSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ThemeSelectorStory {}
+
+impl ThemeSelectorStory {
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ Story::container(cx)
+ .child(Story::title_for::<_, ThemeSelector>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(ThemeSelector::new())
+ }
+}
@@ -42,13 +42,17 @@ pub enum ComponentStory {
CollabPanel,
Facepile,
Keybinding,
+ LanguageSelector,
+ MultiBuffer,
Palette,
Panel,
ProjectPanel,
+ RecentProjects,
StatusBar,
Tab,
TabBar,
Terminal,
+ ThemeSelector,
TitleBar,
Toolbar,
TrafficLights,
@@ -69,15 +73,25 @@ impl ComponentStory {
Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
Self::Facepile => components::facepile::FacepileStory::default().into_any(),
Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
+ Self::LanguageSelector => {
+ components::language_selector::LanguageSelectorStory::default().into_any()
+ }
+ Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(),
Self::Palette => components::palette::PaletteStory::default().into_any(),
Self::Panel => components::panel::PanelStory::default().into_any(),
Self::ProjectPanel => {
components::project_panel::ProjectPanelStory::default().into_any()
}
+ Self::RecentProjects => {
+ components::recent_projects::RecentProjectsStory::default().into_any()
+ }
Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
Self::Tab => components::tab::TabStory::default().into_any(),
Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
Self::Terminal => components::terminal::TerminalStory::default().into_any(),
+ Self::ThemeSelector => {
+ components::theme_selector::ThemeSelectorStory::default().into_any()
+ }
Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
Self::TrafficLights => {
@@ -9,17 +9,22 @@ mod editor_pane;
mod facepile;
mod icon_button;
mod keybinding;
+mod language_selector;
mod list;
+mod multi_buffer;
mod palette;
mod panel;
mod panes;
mod player_stack;
mod project_panel;
+mod recent_projects;
mod status_bar;
mod tab;
mod tab_bar;
mod terminal;
+mod theme_selector;
mod title_bar;
+mod toast;
mod toolbar;
mod traffic_lights;
mod workspace;
@@ -35,17 +40,22 @@ pub use editor_pane::*;
pub use facepile::*;
pub use icon_button::*;
pub use keybinding::*;
+pub use language_selector::*;
pub use list::*;
+pub use multi_buffer::*;
pub use palette::*;
pub use panel::*;
pub use panes::*;
pub use player_stack::*;
pub use project_panel::*;
+pub use recent_projects::*;
pub use status_bar::*;
pub use tab::*;
pub use tab_bar::*;
pub use terminal::*;
+pub use theme_selector::*;
pub use title_bar::*;
+pub use toast::*;
pub use toolbar::*;
pub use traffic_lights::*;
pub use workspace::*;
@@ -0,0 +1,36 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct LanguageSelector {
+ scroll_state: ScrollState,
+}
+
+impl LanguageSelector {
+ pub fn new() -> Self {
+ Self {
+ scroll_state: ScrollState::default(),
+ }
+ }
+
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ div().child(
+ Palette::new(self.scroll_state.clone())
+ .items(vec![
+ PaletteItem::new("C"),
+ PaletteItem::new("C++"),
+ PaletteItem::new("CSS"),
+ PaletteItem::new("Elixir"),
+ PaletteItem::new("Elm"),
+ PaletteItem::new("ERB"),
+ PaletteItem::new("Rust (current)"),
+ PaletteItem::new("Scheme"),
+ PaletteItem::new("TOML"),
+ PaletteItem::new("TypeScript"),
+ ])
+ .placeholder("Select a language...")
+ .empty_string("No matches")
+ .default_order(OrderMethod::Ascending),
+ )
+ }
+}
@@ -135,7 +135,7 @@ impl ListHeader {
.size(IconSize::Small)
}))
.child(
- Label::new(self.label.clone())
+ Label::new(self.label)
.color(LabelColor::Muted)
.size(LabelSize::Small),
),
@@ -191,7 +191,7 @@ impl ListSubHeader {
.size(IconSize::Small)
}))
.child(
- Label::new(self.label.clone())
+ Label::new(self.label)
.color(LabelColor::Muted)
.size(LabelSize::Small),
),
@@ -0,0 +1,42 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize};
+
+#[derive(Element)]
+pub struct MultiBuffer<V: 'static> {
+ view_type: PhantomData<V>,
+ buffers: Vec<Buffer>,
+}
+
+impl<V: 'static> MultiBuffer<V> {
+ pub fn new(buffers: Vec<Buffer>) -> Self {
+ Self {
+ view_type: PhantomData,
+ buffers,
+ }
+ }
+
+ fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let theme = theme(cx);
+
+ v_stack()
+ .w_full()
+ .h_full()
+ .flex_1()
+ .children(self.buffers.clone().into_iter().map(|buffer| {
+ v_stack()
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .p_4()
+ .fill(theme.lowest.base.default.background)
+ .child(Label::new("main.rs").size(LabelSize::Small))
+ .child(IconButton::new(Icon::ArrowUpRight)),
+ )
+ .child(buffer)
+ }))
+ }
+}
@@ -93,19 +93,17 @@ impl<V: 'static> Palette<V> {
.fill(theme.lowest.base.hovered.background)
.active()
.fill(theme.lowest.base.pressed.background)
- .child(
- PaletteItem::new(item.label)
- .keybinding(item.keybinding.clone()),
- )
+ .child(item.clone())
})),
),
)
}
}
-#[derive(Element)]
+#[derive(Element, Clone)]
pub struct PaletteItem {
pub label: &'static str,
+ pub sublabel: Option<&'static str>,
pub keybinding: Option<Keybinding>,
}
@@ -113,6 +111,7 @@ impl PaletteItem {
pub fn new(label: &'static str) -> Self {
Self {
label,
+ sublabel: None,
keybinding: None,
}
}
@@ -122,6 +121,11 @@ impl PaletteItem {
self
}
+ pub fn sublabel<L: Into<Option<&'static str>>>(mut self, sublabel: L) -> Self {
+ self.sublabel = sublabel.into();
+ self
+ }
+
pub fn keybinding<K>(mut self, keybinding: K) -> Self
where
K: Into<Option<Keybinding>>,
@@ -138,7 +142,11 @@ impl PaletteItem {
.flex_row()
.grow()
.justify_between()
- .child(Label::new(self.label))
+ .child(
+ v_stack()
+ .child(Label::new(self.label))
+ .children(self.sublabel.map(|sublabel| Label::new(sublabel))),
+ )
.children(self.keybinding.clone())
}
}
@@ -0,0 +1,32 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct RecentProjects {
+ scroll_state: ScrollState,
+}
+
+impl RecentProjects {
+ pub fn new() -> Self {
+ Self {
+ scroll_state: ScrollState::default(),
+ }
+ }
+
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ div().child(
+ Palette::new(self.scroll_state.clone())
+ .items(vec![
+ PaletteItem::new("zed").sublabel("~/projects/zed"),
+ PaletteItem::new("saga").sublabel("~/projects/saga"),
+ PaletteItem::new("journal").sublabel("~/journal"),
+ PaletteItem::new("dotfiles").sublabel("~/dotfiles"),
+ PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"),
+ PaletteItem::new("laminar").sublabel("~/projects/laminar"),
+ ])
+ .placeholder("Recent Projects...")
+ .empty_string("No matches")
+ .default_order(OrderMethod::Ascending),
+ )
+ }
+}
@@ -0,0 +1,37 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct ThemeSelector {
+ scroll_state: ScrollState,
+}
+
+impl ThemeSelector {
+ pub fn new() -> Self {
+ Self {
+ scroll_state: ScrollState::default(),
+ }
+ }
+
+ fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ div().child(
+ Palette::new(self.scroll_state.clone())
+ .items(vec![
+ PaletteItem::new("One Dark"),
+ PaletteItem::new("Rosé Pine"),
+ PaletteItem::new("Rosé Pine Moon"),
+ PaletteItem::new("Sandcastle"),
+ PaletteItem::new("Solarized Dark"),
+ PaletteItem::new("Summercamp"),
+ PaletteItem::new("Atelier Cave Light"),
+ PaletteItem::new("Atelier Dune Light"),
+ PaletteItem::new("Atelier Estuary Light"),
+ PaletteItem::new("Atelier Forest Light"),
+ PaletteItem::new("Atelier Heath Light"),
+ ])
+ .placeholder("Select Theme...")
+ .empty_string("No matches")
+ .default_order(OrderMethod::Ascending),
+ )
+ }
+}
@@ -0,0 +1,66 @@
+use crate::prelude::*;
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastOrigin {
+ #[default]
+ Bottom,
+ BottomRight,
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastVariant {
+ #[default]
+ Toast,
+ Status,
+}
+
+/// A toast is a small, temporary window that appears to show a message to the user
+/// or indicate a required action.
+///
+/// Toasts should not persist on the screen for more than a few seconds unless
+/// they are actively showing the a process in progress.
+///
+/// Only one toast may be visible at a time.
+#[derive(Element)]
+pub struct Toast<V: 'static> {
+ origin: ToastOrigin,
+ children: HackyChildren<V>,
+ payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Toast<V> {
+ pub fn new(
+ origin: ToastOrigin,
+ children: HackyChildren<V>,
+ payload: HackyChildrenPayload,
+ ) -> Self {
+ Self {
+ origin,
+ children,
+ payload,
+ }
+ }
+
+ fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+ let color = ThemeColor::new(cx);
+
+ let mut div = div();
+
+ if self.origin == ToastOrigin::Bottom {
+ div = div.right_1_2();
+ } else {
+ div = div.right_4();
+ }
+
+ div.absolute()
+ .bottom_4()
+ .flex()
+ .py_2()
+ .px_1p5()
+ .min_w_40()
+ .rounded_md()
+ .fill(color.elevated_surface)
+ .max_w_64()
+ .children_any((self.children)(cx, self.payload.as_ref()))
+ }
+}
@@ -82,6 +82,7 @@ impl WorkspaceElement {
);
div()
+ .relative()
.size_full()
.flex()
.flex_col()
@@ -169,5 +170,17 @@ impl WorkspaceElement {
),
)
.child(StatusBar::new())
+ // An example of a toast is below
+ // Currently because of stacking order this gets obscured by other elements
+
+ // .child(Toast::new(
+ // ToastOrigin::Bottom,
+ // |_, payload| {
+ // let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+ // vec![Label::new("label").into_any()]
+ // },
+ // Box::new(theme.clone()),
+ // ))
}
}
@@ -27,7 +27,7 @@ impl Details {
.gap_0p5()
.text_xs()
.text_color(theme.lowest.base.default.foreground)
- .child(self.text.clone())
+ .child(self.text)
.children(self.meta.map(|m| m))
}
}
@@ -60,6 +60,7 @@ pub enum Icon {
ChevronUp,
Close,
ExclamationTriangle,
+ ExternalLink,
File,
FileGeneric,
FileDoc,
@@ -109,6 +110,7 @@ impl Icon {
Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
+ Icon::ExternalLink => "icons/external_link.svg",
Icon::File => "icons/file.svg",
Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileDoc => "icons/file_icons/book.svg",
@@ -29,6 +29,26 @@ impl SystemColor {
}
}
+#[derive(Clone, Copy)]
+pub struct ThemeColor {
+ pub border: Hsla,
+ pub border_variant: Hsla,
+ /// The background color of an elevated surface, like a modal, tooltip or toast.
+ pub elevated_surface: Hsla,
+}
+
+impl ThemeColor {
+ pub fn new(cx: &WindowContext) -> Self {
+ let theme = theme(cx);
+
+ Self {
+ border: theme.lowest.base.default.border,
+ border_variant: theme.lowest.variant.default.border,
+ elevated_surface: theme.middle.base.default.background,
+ }
+ }
+}
+
#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
pub enum HighlightColor {
#[default]
@@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
column: None,
})
} else {
+ let maybe_col_str =
+ if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
+ &maybe_col_str[..maybe_col_str.len() - 1]
+ } else {
+ maybe_col_str
+ };
match maybe_col_str.parse::<u32>() {
Ok(col) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
@@ -241,7 +247,6 @@ mod tests {
"test_file.rs:1::",
"test_file.rs::1:2",
"test_file.rs:1::2",
- "test_file.rs:1:2:",
"test_file.rs:1:2:3",
] {
let actual = parse_str(input);
@@ -277,6 +282,14 @@ mod tests {
column: None,
},
),
+ (
+ "crates/file_finder/src/file_finder.rs:1902:13:",
+ PathLikeWithPosition {
+ path_like: "crates/file_finder/src/file_finder.rs".to_string(),
+ row: Some(1902),
+ column: Some(13),
+ },
+ ),
];
for (input, expected) in input_and_expected {
@@ -33,7 +33,7 @@ use workspace::{self, Workspace};
use crate::state::ReplayableAction;
-struct VimModeSetting(bool);
+pub struct VimModeSetting(pub bool);
#[derive(Clone, Deserialize, PartialEq)]
pub struct SwitchMode(pub Mode);
@@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
picker = { path = "../picker" }
workspace = { path = "../workspace" }
+vim = { path = "../vim" }
anyhow.workspace = true
log.workspace = true
@@ -10,6 +10,7 @@ use gpui::{
};
use settings::{update_settings_file, SettingsStore};
use std::{borrow::Cow, sync::Arc};
+use vim::VimModeSetting;
use workspace::{
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId,
@@ -65,6 +66,7 @@ impl View for WelcomePage {
let width = theme.welcome.page_width;
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+ let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
enum Metrics {}
enum Diagnostics {}
@@ -144,6 +146,27 @@ impl View for WelcomePage {
)
.with_child(
Flex::column()
+ .with_child(
+ theme::ui::checkbox::<Diagnostics, Self, _>(
+ "Enable vim mode",
+ &theme.welcome.checkbox,
+ vim_mode_setting,
+ 0,
+ cx,
+ |this, checked, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ update_settings_file::<VimModeSetting>(
+ fs,
+ cx,
+ move |setting| *setting = Some(checked),
+ )
+ }
+ },
+ )
+ .contained()
+ .with_style(theme.welcome.checkbox_container),
+ )
.with_child(
theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
Flex::column()
@@ -186,7 +209,7 @@ impl View for WelcomePage {
"Send crash reports",
&theme.welcome.checkbox,
telemetry_settings.diagnostics,
- 0,
+ 1,
cx,
|this, checked, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
@@ -22,7 +22,6 @@ test-support = [
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
-channel = { path = "../channel" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
@@ -1,10 +1,7 @@
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-
-use crate::{
- pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
-};
+use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
+use collections::HashMap;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::Vector2F},
@@ -13,6 +10,7 @@ use gpui::{
};
use project::Project;
use serde::Deserialize;
+use std::{cell::RefCell, rc::Rc, sync::Arc};
use theme::Theme;
const HANDLE_HITBOX_SIZE: f32 = 4.0;
@@ -95,7 +93,7 @@ impl PaneGroup {
&self,
project: &ModelHandle<Project>,
theme: &Theme,
- follower_states: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -162,7 +160,7 @@ impl Member {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
- follower_states: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -179,19 +177,10 @@ impl Member {
ChildView::new(pane, cx).into_any()
};
- let leader = follower_states
- .iter()
- .find_map(|(leader_id, follower_states)| {
- if follower_states.contains_key(pane) {
- Some(leader_id)
- } else {
- None
- }
- })
- .and_then(|leader_id| {
- let room = active_call?.read(cx).room()?.read(cx);
- room.remote_participant_for_peer_id(*leader_id)
- });
+ let leader = follower_states.get(pane).and_then(|state| {
+ let room = active_call?.read(cx).room()?.read(cx);
+ room.remote_participant_for_peer_id(state.leader_id)
+ });
let mut leader_border = Border::default();
let mut leader_status_box = None;
@@ -486,7 +475,7 @@ impl PaneAxis {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
- follower_state: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -515,7 +504,7 @@ impl PaneAxis {
project,
(basis + ix) * 10,
theme,
- follower_state,
+ follower_states,
active_call,
active_pane,
zoomed,
@@ -12,7 +12,6 @@ mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
-use channel::ChannelStore;
use client::{
proto::{self, PeerId},
Client, Status, TypedEnvelope, UserStore,
@@ -79,7 +78,7 @@ use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::{Theme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::{async_iife, ResultExt};
+use util::ResultExt;
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
@@ -450,7 +449,6 @@ pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<Client>,
pub user_store: ModelHandle<UserStore>,
- pub channel_store: ModelHandle<ChannelStore>,
pub workspace_store: ModelHandle<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
@@ -487,8 +485,6 @@ impl AppState {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init((), cx);
@@ -500,7 +496,7 @@ impl AppState {
fs,
languages,
user_store,
- channel_store,
+ // channel_store,
workspace_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
@@ -573,11 +569,12 @@ pub struct Workspace {
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<WeakViewHandle<Pane>>,
+ last_active_view_id: Option<proto::ViewId>,
status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
- follower_states_by_leader: FollowerStatesByLeader,
+ follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
@@ -602,10 +599,9 @@ pub struct ViewId {
pub id: u64,
}
-type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
-
#[derive(Default)]
struct FollowerState {
+ leader_id: PeerId,
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
@@ -786,6 +782,7 @@ impl Workspace {
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
+ last_active_view_id: None,
status_bar,
titlebar_item: None,
notifications: Default::default(),
@@ -793,7 +790,7 @@ impl Workspace {
bottom_dock,
right_dock,
project: project.clone(),
- follower_states_by_leader: Default::default(),
+ follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
@@ -934,7 +931,8 @@ impl Workspace {
app_state,
cx,
)
- .await;
+ .await
+ .unwrap_or_default();
(workspace, opened_items)
})
@@ -2510,13 +2508,16 @@ impl Workspace {
}
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
- for state in states_by_pane.into_values() {
- for item in state.items_by_leader_view_id.into_values() {
+ self.follower_states.retain(|_, state| {
+ if state.leader_id == peer_id {
+ for item in state.items_by_leader_view_id.values() {
item.set_leader_peer_id(None, cx);
}
+ false
+ } else {
+ true
}
- }
+ });
cx.notify();
}
@@ -2529,10 +2530,15 @@ impl Workspace {
self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id);
- self.follower_states_by_leader
- .entry(leader_id)
- .or_default()
- .insert(pane.clone(), Default::default());
+ self.unfollow(&pane, cx);
+ self.follower_states.insert(
+ pane.clone(),
+ FollowerState {
+ leader_id,
+ active_view_id: None,
+ items_by_leader_view_id: Default::default(),
+ },
+ );
cx.notify();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
@@ -2547,9 +2553,8 @@ impl Workspace {
let response = request.await?;
this.update(&mut cx, |this, _| {
let state = this
- .follower_states_by_leader
- .get_mut(&leader_id)
- .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
+ .follower_states
+ .get_mut(&pane)
.ok_or_else(|| anyhow!("following interrupted"))?;
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
Some(ViewId::from_proto(active_view_id)?)
@@ -2644,12 +2649,10 @@ impl Workspace {
}
// 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;
- }
+ for (pane, state) in &self.follower_states {
+ if leader_id == state.leader_id {
+ cx.focus(pane);
+ return None;
}
}
@@ -2662,36 +2665,37 @@ impl Workspace {
pane: &ViewHandle<Pane>,
cx: &mut ViewContext<Self>,
) -> Option<PeerId> {
- for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
- let leader_id = *leader_id;
- if let Some(state) = states_by_pane.remove(pane) {
- for (_, item) in state.items_by_leader_view_id {
- item.set_leader_peer_id(None, cx);
- }
-
- if states_by_pane.is_empty() {
- self.follower_states_by_leader.remove(&leader_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
- .client
- .send(proto::Unfollow {
- room_id,
- project_id,
- leader_id: Some(leader_id),
- })
- .log_err();
- }
+ let state = self.follower_states.remove(pane)?;
+ let leader_id = state.leader_id;
+ for (_, item) in state.items_by_leader_view_id {
+ item.set_leader_peer_id(None, cx);
+ }
- cx.notify();
- return Some(leader_id);
- }
+ if self
+ .follower_states
+ .values()
+ .all(|state| state.leader_id != state.leader_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
+ .client
+ .send(proto::Unfollow {
+ room_id,
+ project_id,
+ leader_id: Some(leader_id),
+ })
+ .log_err();
}
- None
+
+ cx.notify();
+ Some(leader_id)
}
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
- self.follower_states_by_leader.contains_key(&peer_id)
+ self.follower_states
+ .values()
+ .any(|state| state.leader_id == peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -2862,6 +2866,7 @@ impl Workspace {
cx.notify();
+ self.last_active_view_id = active_view_id.clone();
proto::FollowResponse {
active_view_id,
views: self
@@ -2913,8 +2918,8 @@ impl Workspace {
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(cx, |this, _| {
- if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
- for state in state.values_mut() {
+ for (_, state) in &mut this.follower_states {
+ if state.leader_id == leader_id {
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
@@ -2936,8 +2941,8 @@ impl Workspace {
let mut tasks = Vec::new();
this.update(cx, |this, cx| {
let project = this.project.clone();
- if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
- for state in state.values_mut() {
+ for (_, state) in &mut this.follower_states {
+ if state.leader_id == leader_id {
let view_id = ViewId::from_proto(id.clone())?;
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
@@ -2950,10 +2955,9 @@ impl Workspace {
}
proto::update_followers::Variant::CreateView(view) => {
let panes = this.read_with(cx, |this, _| {
- this.follower_states_by_leader
- .get(&leader_id)
- .into_iter()
- .flat_map(|states_by_pane| states_by_pane.keys())
+ this.follower_states
+ .iter()
+ .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
.cloned()
.collect()
})?;
@@ -3012,11 +3016,7 @@ impl Workspace {
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
let items = futures::future::try_join_all(item_tasks).await?;
this.update(cx, |this, cx| {
- let state = this
- .follower_states_by_leader
- .get_mut(&leader_id)?
- .get_mut(&pane)?;
-
+ let state = this.follower_states.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_peer_id(Some(leader_id), cx);
state.items_by_leader_view_id.insert(id, item);
@@ -3028,7 +3028,7 @@ impl Workspace {
Ok(())
}
- fn update_active_view_for_followers(&self, cx: &AppContext) {
+ fn update_active_view_for_followers(&mut self, cx: &AppContext) {
let mut is_project_item = true;
let mut update = proto::UpdateActiveView::default();
if self.active_pane.read(cx).has_focus() {
@@ -3046,11 +3046,14 @@ impl Workspace {
}
}
- self.update_followers(
- is_project_item,
- proto::update_followers::Variant::UpdateActiveView(update),
- cx,
- );
+ if update.id != self.last_active_view_id {
+ self.last_active_view_id = update.id.clone();
+ self.update_followers(
+ is_project_item,
+ proto::update_followers::Variant::UpdateActiveView(update),
+ cx,
+ );
+ }
}
fn update_followers(
@@ -3070,15 +3073,7 @@ impl Workspace {
}
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
- self.follower_states_by_leader
- .iter()
- .find_map(|(leader_id, state)| {
- if state.contains_key(pane) {
- Some(*leader_id)
- } else {
- None
- }
- })
+ self.follower_states.get(pane).map(|state| state.leader_id)
}
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3106,17 +3101,23 @@ impl Workspace {
}
};
- 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 {
+ for (pane, state) in &self.follower_states {
+ if state.leader_id != leader_id {
+ continue;
+ }
+ if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
+ if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
if leader_in_this_project || !item.is_project_item(cx) {
items_to_activate.push((pane.clone(), item.boxed_clone()));
}
- continue;
+ } else {
+ log::warn!(
+ "unknown view id {:?} for leader {:?}",
+ active_view_id,
+ leader_id
+ );
}
+ continue;
}
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
@@ -3394,140 +3395,124 @@ impl Workspace {
serialized_workspace: SerializedWorkspace,
paths_to_open: Vec<Option<ProjectPath>>,
cx: &mut AppContext,
- ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
+ ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
cx.spawn(|mut cx| async move {
- let result = async_iife! {{
- let (project, old_center_pane) =
- workspace.read_with(&cx, |workspace, _| {
- (
- workspace.project().clone(),
- workspace.last_active_center_pane.clone(),
- )
- })?;
+ let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
+ (
+ workspace.project().clone(),
+ workspace.last_active_center_pane.clone(),
+ )
+ })?;
- let mut center_items = None;
- let mut center_group = None;
- // Traverse the splits tree and add to things
- if let Some((group, active_pane, items)) = serialized_workspace
- .center_group
- .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
- .await {
- center_items = Some(items);
- center_group = Some((group, active_pane))
- }
+ let mut center_group = None;
+ let mut center_items = None;
+ // Traverse the splits tree and add to things
+ if let Some((group, active_pane, items)) = serialized_workspace
+ .center_group
+ .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
+ .await
+ {
+ center_items = Some(items);
+ center_group = Some((group, active_pane))
+ }
- let resulting_list = cx.read(|cx| {
- let mut opened_items = center_items
- .unwrap_or_default()
- .into_iter()
- .filter_map(|item| {
- let item = item?;
- let project_path = item.project_path(cx)?;
- Some((project_path, item))
- })
- .collect::<HashMap<_, _>>();
+ let mut items_by_project_path = cx.read(|cx| {
+ center_items
+ .unwrap_or_default()
+ .into_iter()
+ .filter_map(|item| {
+ let item = item?;
+ let project_path = item.project_path(cx)?;
+ Some((project_path, item))
+ })
+ .collect::<HashMap<_, _>>()
+ });
- paths_to_open
- .into_iter()
- .map(|path_to_open| {
- path_to_open.map(|path_to_open| {
- Ok(opened_items.remove(&path_to_open))
- })
- .transpose()
- .map(|item| item.flatten())
- .transpose()
- })
- .collect::<Vec<_>>()
- });
+ let opened_items = paths_to_open
+ .into_iter()
+ .map(|path_to_open| {
+ path_to_open
+ .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
+ })
+ .collect::<Vec<_>>();
- // Remove old panes from workspace panes list
- workspace.update(&mut cx, |workspace, cx| {
- if let Some((center_group, active_pane)) = center_group {
- workspace.remove_panes(workspace.center.root.clone(), cx);
+ // Remove old panes from workspace panes list
+ workspace.update(&mut cx, |workspace, cx| {
+ if let Some((center_group, active_pane)) = center_group {
+ workspace.remove_panes(workspace.center.root.clone(), cx);
- // Swap workspace center group
- workspace.center = PaneGroup::with_root(center_group);
+ // Swap workspace center group
+ workspace.center = PaneGroup::with_root(center_group);
- // Change the focus to the workspace first so that we retrigger focus in on the pane.
- cx.focus_self();
+ // Change the focus to the workspace first so that we retrigger focus in on the pane.
+ cx.focus_self();
- if let Some(active_pane) = active_pane {
- cx.focus(&active_pane);
- } else {
- cx.focus(workspace.panes.last().unwrap());
- }
+ if let Some(active_pane) = active_pane {
+ cx.focus(&active_pane);
} else {
- let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
- if let Some(old_center_handle) = old_center_handle {
- cx.focus(&old_center_handle)
- } else {
- cx.focus_self()
- }
+ cx.focus(workspace.panes.last().unwrap());
}
+ } else {
+ let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
+ if let Some(old_center_handle) = old_center_handle {
+ cx.focus(&old_center_handle)
+ } else {
+ cx.focus_self()
+ }
+ }
- let docks = serialized_workspace.docks;
- workspace.left_dock.update(cx, |dock, cx| {
- dock.set_open(docks.left.visible, cx);
- if let Some(active_panel) = docks.left.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
- }
- }
- dock.active_panel()
- .map(|panel| {
- panel.set_zoomed(docks.left.zoom, cx)
- });
- if docks.left.visible && docks.left.zoom {
- cx.focus_self()
- }
- });
- // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
- workspace.right_dock.update(cx, |dock, cx| {
- dock.set_open(docks.right.visible, cx);
- if let Some(active_panel) = docks.right.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
-
- }
+ let docks = serialized_workspace.docks;
+ workspace.left_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.left.visible, cx);
+ if let Some(active_panel) = docks.left.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
}
- dock.active_panel()
- .map(|panel| {
- panel.set_zoomed(docks.right.zoom, cx)
- });
-
- if docks.right.visible && docks.right.zoom {
- cx.focus_self()
- }
- });
- workspace.bottom_dock.update(cx, |dock, cx| {
- dock.set_open(docks.bottom.visible, cx);
- if let Some(active_panel) = docks.bottom.active_panel {
- if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
- }
+ }
+ dock.active_panel()
+ .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
+ if docks.left.visible && docks.left.zoom {
+ cx.focus_self()
+ }
+ });
+ // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
+ workspace.right_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.right.visible, cx);
+ if let Some(active_panel) = docks.right.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
}
+ }
+ dock.active_panel()
+ .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
- dock.active_panel()
- .map(|panel| {
- panel.set_zoomed(docks.bottom.zoom, cx)
- });
-
- if docks.bottom.visible && docks.bottom.zoom {
- cx.focus_self()
+ if docks.right.visible && docks.right.zoom {
+ cx.focus_self()
+ }
+ });
+ workspace.bottom_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.bottom.visible, cx);
+ if let Some(active_panel) = docks.bottom.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
}
- });
+ }
+ dock.active_panel()
+ .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
- cx.notify();
- })?;
+ if docks.bottom.visible && docks.bottom.zoom {
+ cx.focus_self()
+ }
+ });
- // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
- workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
+ cx.notify();
+ })?;
- Ok::<_, anyhow::Error>(resulting_list)
- }};
+ // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
+ workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
- result.await.unwrap_or_default()
+ Ok(opened_items)
})
}
@@ -3536,15 +3521,12 @@ impl Workspace {
let client = project.read(cx).client();
let user_store = project.read(cx).user_store();
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
workspace_store,
client,
user_store,
- channel_store,
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -3601,7 +3583,7 @@ async fn open_items(
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
app_state: Arc<AppState>,
mut cx: AsyncAppContext,
-) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
+) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
if let Some(serialized_workspace) = serialized_workspace {
@@ -3619,16 +3601,19 @@ async fn open_items(
cx,
)
})
- .await;
+ .await?;
let restored_project_paths = cx.read(|cx| {
restored_items
.iter()
- .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
+ .filter_map(|item| item.as_ref()?.project_path(cx))
.collect::<HashSet<_>>()
});
- opened_items = restored_items;
+ for restored_item in restored_items {
+ opened_items.push(restored_item.map(Ok));
+ }
+
project_paths_to_open
.iter_mut()
.for_each(|(_, project_path)| {
@@ -3681,7 +3666,7 @@ async fn open_items(
}
}
- opened_items
+ Ok(opened_items)
}
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
@@ -3817,7 +3802,7 @@ impl View for Workspace {
self.center.render(
&project,
&theme,
- &self.follower_states_by_leader,
+ &self.follower_states,
self.active_call(),
self.active_pane(),
self.zoomed
@@ -165,17 +165,25 @@ impl LspAdapter for RustLspAdapter {
lazy_static! {
static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
}
-
let detail = completion.detail.as_ref().unwrap();
- if detail.starts_with("fn(") {
- let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
- let source = Rope::from(format!("fn {} {{}}", text).as_str());
- let runs = language.highlight_text(&source, 3..3 + text.len());
- return Some(CodeLabel {
- filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
- text,
- runs,
- });
+ const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
+ let prefix = FUNCTION_PREFIXES
+ .iter()
+ .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
+ // fn keyword should be followed by opening parenthesis.
+ if let Some((prefix, suffix)) = prefix {
+ if suffix.starts_with('(') {
+ let text = REGEX.replace(&completion.label, suffix).to_string();
+ let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
+ let run_start = prefix.len() + 1;
+ let runs =
+ language.highlight_text(&source, run_start..run_start + text.len());
+ return Some(CodeLabel {
+ filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+ text,
+ runs,
+ });
+ }
}
}
Some(kind) => {
@@ -377,7 +385,28 @@ mod tests {
],
})
);
-
+ assert_eq!(
+ language
+ .label_for_completion(&lsp::CompletionItem {
+ kind: Some(lsp::CompletionItemKind::FUNCTION),
+ label: "hello(…)".to_string(),
+ detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+ ..Default::default()
+ })
+ .await,
+ Some(CodeLabel {
+ text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+ filter_range: 0..5,
+ runs: vec![
+ (0..5, highlight_function),
+ (7..10, highlight_keyword),
+ (11..17, highlight_type),
+ (18..19, highlight_type),
+ (25..28, highlight_type),
+ (29..30, highlight_type),
+ ],
+ })
+ );
assert_eq!(
language
.label_for_completion(&lsp::CompletionItem {
@@ -3,7 +3,6 @@
use anyhow::{anyhow, Context, Result};
use backtrace::Backtrace;
-use channel::ChannelStore;
use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
@@ -78,7 +77,8 @@ fn main() {
let mut app = gpui::App::new(Assets).unwrap();
let installation_id = app.background().block(installation_id()).ok();
- init_panic_hook(&app, installation_id.clone());
+ let session_id = Uuid::new_v4().to_string();
+ init_panic_hook(&app, installation_id.clone(), session_id.clone());
load_embedded_fonts(&app);
@@ -132,8 +132,6 @@ fn main() {
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
cx.set_global(client.clone());
@@ -150,7 +148,7 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
- channel::init(&client);
+ channel::init(&client, user_store.clone(), cx);
diagnostics::init(cx);
search::init(cx);
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -172,13 +170,12 @@ fn main() {
})
.detach();
- client.telemetry().start(installation_id, cx);
+ client.telemetry().start(installation_id, session_id, cx);
let app_state = Arc::new(AppState {
languages,
client: client.clone(),
user_store,
- channel_store,
fs,
build_window_options,
initialize_workspace,
@@ -387,6 +384,7 @@ struct Panic {
panicked_on: u128,
#[serde(skip_serializing_if = "Option::is_none")]
installation_id: Option<String>,
+ session_id: String,
}
#[derive(Serialize)]
@@ -397,7 +395,7 @@ struct PanicRequest {
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
-fn init_panic_hook(app: &App, installation_id: Option<String>) {
+fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: String) {
let is_pty = stdout_is_a_pty();
let platform = app.platform();
@@ -462,7 +460,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
line: location.line(),
}),
app_version: app_version.clone(),
- release_channel: RELEASE_CHANNEL.dev_name().into(),
+ release_channel: RELEASE_CHANNEL.display_name().into(),
os_name: platform.os_name().into(),
os_version: platform
.os_version()
@@ -475,13 +473,14 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
.as_millis(),
backtrace,
installation_id: installation_id.clone(),
+ session_id: session_id.clone(),
};
- if is_pty {
- if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
- eprintln!("{}", panic_data_json);
- }
- } else {
+ if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
+ log::error!("{}", panic_data_json);
+ }
+
+ if !is_pty {
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
@@ -3,8 +3,6 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse};
use futures::channel::mpsc;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use std::ffi::OsStr;
-use std::fs::OpenOptions;
-use std::io::Write;
use std::os::unix::prelude::OsStrExt;
use std::sync::atomic::Ordering;
use std::{path::PathBuf, sync::atomic::AtomicBool};
@@ -2424,6 +2424,7 @@ mod tests {
state.build_window_options = build_window_options;
theme::init((), cx);
audio::init((), cx);
+ channel::init(&app_state.client, app_state.user_store.clone(), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
@@ -75,8 +75,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil
- If you are just using the latest version, but not working on zed:
- `cargo run --release`
- If you need to run the collaboration server locally:
- - `script/zed-with-local-servers`
- - If you need to test collaboration with mutl
+ - `script/zed-local`
## Troubleshooting
@@ -17,6 +17,6 @@
## Testing collab locally
1. Run `foreman start` from the root of the repo.
-1. In another terminal run `script/start-local-collaboration`.
+1. In another terminal run `script/zed-local -2`.
1. Two copies of Zed will open. Add yourself as a contact in the one that is not you.
1. Start a collaboration session as normal with any open project.
@@ -1,4 +1,4 @@
[toolchain]
-channel = "1.72.1"
+channel = "1.73"
components = [ "rustfmt" ]
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -e
+
+if [[ -x cargo-depgraph ]]; then
+ cargo install cargo-depgraph
+fi
+
+graph_file=target/crate-graph.html
+
+cargo depgraph \
+ --workspace-only \
+ --offline \
+ --root=zed,cli,collab \
+ --dedup-transitive-deps \
+ | dot -Tsvg > $graph_file
+
+echo "open $graph_file"
+open $graph_file
@@ -1,59 +0,0 @@
-#!/bin/bash
-
-set -e
-
-if [[ -z "$GITHUB_TOKEN" ]]; then
- cat <<-MESSAGE
-Missing \`GITHUB_TOKEN\` environment variable. This token is needed
-for fetching your GitHub identity from the command-line.
-
-Create an access token here: https://github.com/settings/tokens
-Then edit your \`~/.zshrc\` (or other shell initialization script),
-adding a line like this:
-
- export GITHUB_TOKEN="(the token)"
-
-MESSAGE
- exit 1
-fi
-
-# Install jq if it's not installed
-if ! command -v jq &> /dev/null; then
- echo "Installing jq..."
- brew install jq
-fi
-
-# Start one Zed instance as the current user and a second instance with a different user.
-username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login)
-username_2=nathansobo
-if [[ $username_1 == $username_2 ]]; then
- username_2=as-cii
-fi
-
-# Make each Zed instance take up half of the screen.
-output=$(system_profiler SPDisplaysDataType -json)
-main_display=$(echo "$output" | jq '.SPDisplaysDataType[].spdisplays_ndrvs[] | select(.spdisplays_main == "spdisplays_yes")')
-resolution=$(echo "$main_display" | jq -r '._spdisplays_resolution')
-width=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[0].string')
-half_width=$(($width / 2))
-height=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[1].string')
-y=0
-
-position_1=0,${y}
-position_2=${half_width},${y}
-
-# Authenticate using the collab server's admin secret.
-export ZED_STATELESS=1
-export ZED_ALWAYS_ACTIVE=1
-export ZED_ADMIN_API_TOKEN=secret
-export ZED_SERVER_URL=http://localhost:8080
-export ZED_WINDOW_SIZE=${half_width},${height}
-
-cargo build
-sleep 0.5
-
-# Start the two Zed child processes. Open the given paths with the first instance.
-trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
-ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
-SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
-wait
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+
+const {spawn, execFileSync} = require('child_process')
+
+const RESOLUTION_REGEX = /(\d+) x (\d+)/
+const DIGIT_FLAG_REGEX = /^--?(\d+)$/
+
+const args = process.argv.slice(2)
+
+// Parse the number of Zed instances to spawn.
+let instanceCount = 1
+const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX)
+if (digitMatch) {
+ instanceCount = parseInt(digitMatch[1])
+ args.shift()
+}
+if (instanceCount > 4) {
+ throw new Error('Cannot spawn more than 4 instances')
+}
+
+// Parse the resolution of the main screen
+const displayInfo = JSON.parse(
+ execFileSync(
+ 'system_profiler',
+ ['SPDisplaysDataType', '-json'],
+ {encoding: 'utf8'}
+ )
+)
+const mainDisplayResolution = displayInfo
+ ?.SPDisplaysDataType[0]
+ ?.spdisplays_ndrvs
+ ?.find(entry => entry.spdisplays_main === "spdisplays_yes")
+ ?._spdisplays_resolution
+ ?.match(RESOLUTION_REGEX)
+if (!mainDisplayResolution) {
+ throw new Error('Could not parse screen resolution')
+}
+const screenWidth = parseInt(mainDisplayResolution[1])
+const screenHeight = parseInt(mainDisplayResolution[2])
+
+// Determine the window size for each instance
+let instanceWidth = screenWidth
+let instanceHeight = screenHeight
+if (instanceCount > 1) {
+ instanceWidth = Math.floor(screenWidth / 2)
+ if (instanceCount > 2) {
+ instanceHeight = Math.floor(screenHeight / 2)
+ }
+}
+
+let users = [
+ 'nathansobo',
+ 'as-cii',
+ 'maxbrunsfeld',
+ 'iamnbutler'
+]
+
+// If a user is specified, make sure it's first in the list
+const user = process.env.ZED_IMPERSONATE
+if (user) {
+ users = [user].concat(users.filter(u => u !== user))
+}
+
+const positions = [
+ '0,0',
+ `${instanceWidth},0`,
+ `0,${instanceHeight}`,
+ `${instanceWidth},${instanceHeight}`
+]
+
+execFileSync('cargo', ['build'], {stdio: 'inherit'})
+
+setTimeout(() => {
+ for (let i = 0; i < instanceCount; i++) {
+ spawn('target/debug/Zed', i == 0 ? args : [], {
+ stdio: 'inherit',
+ env: {
+ ZED_IMPERSONATE: users[i],
+ ZED_WINDOW_POSITION: positions[i],
+ ZED_STATELESS: '1',
+ ZED_ALWAYS_ACTIVE: '1',
+ ZED_SERVER_URL: 'http://localhost:8080',
+ ZED_ADMIN_API_TOKEN: 'secret',
+ ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`
+ }
+ })
+ }
+}, 0.1)
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-: "${ZED_IMPERSONATE:=as-cii}"
-export ZED_IMPERSONATE
-
-ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@