Merge branch 'main' into links

This commit is contained in:
Conrad Irwin 2023-10-09 19:59:21 -06:00
commit d4ef764305
66 changed files with 1668 additions and 657 deletions

10
Cargo.lock generated
View file

@ -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",

View file

@ -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 . .

View file

@ -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

View file

@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }

View file

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

View file

@ -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,

View file

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

View file

@ -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>,

View file

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

View file

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

View file

@ -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();
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,
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,
},
)?;
release_channel: state.release_channel,
events,
};
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client

View file

@ -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]]

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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);
});
workspace_b.read_with(cx_b, |workspace, _| {
assert_eq!(*workspace.active_pane(), pane_b1);
});
// Ensure peers following each other doesn't cause an infinite loop.
// Both client's see the other client open the new file, but keep their
// focus on their own active pane.
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_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())
]
},
]
);
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);
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);
});
});
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();
});
});
deterministic.run_until_parked();
// Both clients see that Client B is looking at the previous tab.
assert_eq!(
pane_summaries(&workspace_b, cx_b),
&[
PaneSummary {
active: false,
leader: None,
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
},
PaneSummary {
active: true,
leader: None,
items: vec![(true, "3.txt".into()),]
},
]
);
assert_eq!(
pane_summaries(&workspace_a, cx_a),
&[
PaneSummary {
active: false,
leader: None,
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
},
PaneSummary {
active: true,
leader: client_b.peer_id(),
items: vec![
(false, "1.txt".into()),
(false, "2.txt".into()),
(false, "4.txt".into()),
(true, "3.txt".into()),
]
},
]
);
// Client B follows client A again.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
})
.await
.unwrap();
// Client A cycles through some tabs.
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, cx);
});
});
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()
})
}

View file

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

View file

@ -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

View file

@ -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| {

View file

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

View file

@ -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,

View file

@ -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

View file

@ -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)]

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -22,7 +22,6 @@ test-support = [
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }

View file

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

View file

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

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 => {

View file

@ -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::*;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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",

View file

@ -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]

View file

@ -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 {

View file

@ -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);

View file

@ -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

View file

@ -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) {

View file

@ -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" }

View file

@ -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,

View file

@ -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();
}
cx.notify();
return Some(leader_id);
}
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);
}
None
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();
}
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 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<_, _>>()
});
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);
// 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();
if let Some(active_pane) = active_pane {
cx.focus(&active_pane);
} else {
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 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<_, _>>();
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<_>>()
});
// 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);
// 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());
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);
}
} 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()
}
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));
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);
}
}
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);
dock.active_panel()
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
}
}
dock.active_panel()
.map(|panel| {
panel.set_zoomed(docks.right.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);
}
}
cx.notify();
})?;
dock.active_panel()
.map(|panel| {
panel.set_zoomed(docks.bottom.zoom, cx)
});
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
if docks.bottom.visible && docks.bottom.zoom {
cx.focus_self()
}
});
cx.notify();
})?;
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
Ok::<_, anyhow::Error>(resulting_list)
}};
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

View file

@ -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 {

View file

@ -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));

View file

@ -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};

View file

@ -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);

View file

@ -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

View file

@ -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.

View file

@ -1,4 +1,4 @@
[toolchain]
channel = "1.72.1"
channel = "1.73"
components = [ "rustfmt" ]
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

19
script/crate-dep-graph Executable file
View file

@ -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

View 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

88
script/zed-local Executable file
View file

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

View file

@ -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 $@