Compare commits

...

83 Commits

Author SHA1 Message Date
Max Brunsfeld
c8b209306e collab 0.4.2 2022-12-19 11:29:22 -08:00
Max Brunsfeld
61c6c825b5 Merge pull request #1980 from zed-industries/following-panics
Fix panics when following
2022-12-19 11:26:28 -08:00
Julia
6f211292b2 Merge pull request #1984 from zed-industries/format-problematic-db-macros
Format problematic DB macros
2022-12-19 11:17:34 -05:00
Julia
c49573dc11 Format problematic DB macros 2022-12-19 11:11:10 -05:00
Julia
de9c58d216 Merge pull request #1983 from zed-industries/multi-buffer-git-gutter
Multi buffer git gutter
2022-12-19 10:53:42 -05:00
Antonio Scandurra
84a860e54d Merge pull request #1982 from zed-industries/fix-rust-analyzer
Update rust-analyzer's `disk_based_diagnostics_progress_token`
2022-12-19 16:33:01 +01:00
Antonio Scandurra
cb60eb8a57 Update rust-analyzer's disk_based_diagnostics_progress_token 2022-12-19 16:27:25 +01:00
Max Brunsfeld
1e02ebbd11 Replicate pending selections separately from other selections
This fixes a panic that would occur when a leader created
a pending selection that overlapped another selection,
because the follower would attempt to treat that pending
selection as non-pending, which would violate the invariant
that selections are sorted and disjoint.
2022-12-17 14:00:53 -08:00
Max Brunsfeld
8c64514570 Add ZED_STATELESS env var, for suppressing persistence
Use this env var in the start-local-collaboration script to make
the behavior more predictable.
2022-12-17 12:03:51 -08:00
Kay Simmons
6fcb3c9020 Merge pull request #1972 from zed-industries/recent-workspace
Recent Project Picker
2022-12-16 15:51:57 -08:00
Kay Simmons
2c47bd4a97 Clear stale projects if they no longer exist 2022-12-16 15:45:17 -08:00
Antonio Scandurra
a5f624203e collab 0.4.1 2022-12-16 12:02:03 +01:00
Antonio Scandurra
98d1b6ec5a Merge pull request #1975 from zed-industries/screen-share-after-reconnect
Prevent screen-sharing from being lost after a reconnection
2022-12-16 12:00:02 +01:00
Antonio Scandurra
457e1046c8 Bump protocol version 2022-12-16 11:48:14 +01:00
Antonio Scandurra
21ab1bb434 Remove unnecessary PeerId parsing code 2022-12-16 11:45:42 +01:00
Antonio Scandurra
aa44de3d16 Fix test ensuring room is left when disconnected from LiveKit 2022-12-16 10:52:32 +01:00
Max Brunsfeld
ad37034960 Identify LiveKit room participants by user id, not peer id
This way, their participant id can remain the same when they reconnect.
2022-12-15 17:19:32 -08:00
Julia
ebd0c5d000 Handle reversed=true for multi-buffer git-hunks-in-range iteration
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-12-15 18:17:32 -05:00
Julia
f88b413f6a Rewrite multi-buffer aware git hunks in range to be more correct
Less ad-hoc state tracking, rely more on values provided by the
underlying data

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-15 17:09:09 -05:00
Antonio Scandurra
c2f5381e5a collab 0.4.0 2022-12-15 19:37:53 +01:00
Antonio Scandurra
ea1f6689b9 Merge pull request #1971 from zed-industries/update-app-icons
Update Zed & Zed Preview icons
2022-12-15 19:37:04 +01:00
Antonio Scandurra
b1affb13bb Merge pull request #1973 from zed-industries/fix-reconnects-after-deploy
Improve reconnections to server after it is redeployed
2022-12-15 19:35:42 +01:00
Antonio Scandurra
2679e245a5 Minor stylistic change 2022-12-15 16:40:16 +01:00
Antonio Scandurra
5a334622ea 💄 2022-12-15 16:34:59 +01:00
Antonio Scandurra
5720c43fe7 Merge branch 'main' into fix-reconnects-after-deploy 2022-12-15 15:32:05 +01:00
Joseph T. Lyons
af4d846428 Merge pull request #1954 from zed-industries/add-symlink-to-applications-directory-in-dmg 2022-12-15 08:06:17 -05:00
Antonio Scandurra
5fb522a9b1 collab 0.3.14 2022-12-15 11:31:51 +01:00
Antonio Scandurra
86e5ae1f2e Allow nulls in projects.host_connection_{id,server_id}
The server version on stable won't be able to fill values for those
columns when we deploy the migration to preview.

With this commit we're also dropping the unused `worktree_extensions`
and `project_activity_periods` tables. The last version of the server
on stable (0.2.6) doesn't contain any code that accesses those tables.
2022-12-15 11:30:51 +01:00
Antonio Scandurra
aadd7f2886 collab 0.3.13 2022-12-15 10:53:17 +01:00
Antonio Scandurra
067a19c971 Avoid logging an error when user who hasn't joined any room disconnects 2022-12-15 10:45:03 +01:00
Antonio Scandurra
688f179256 Use "id" nomenclature more consistently 2022-12-15 10:15:59 +01:00
Antonio Scandurra
af77f1188a Re-add server_id indices for room_participants/project_collaborators 2022-12-15 09:58:25 +01:00
Julia
0dedc1f3a4 Get tests building again 2022-12-15 00:17:28 -05:00
Max Brunsfeld
6c58a4f885 Fix stale server queries, use foreign keys from connectionsn to servers 2022-12-14 17:34:24 -08:00
Kay Simmons
81e3b48f37 Add keybinding 2022-12-14 16:14:16 -08:00
Kay Simmons
6da59311d1 Add open recent project to file menu 2022-12-14 16:02:48 -08:00
Kay Simmons
2bc685281c Add recent project picker 2022-12-14 15:59:50 -08:00
Max Brunsfeld
7e0b6ed1c6 Bump RPC version due to multibuffer following PR 2022-12-14 15:34:22 -08:00
Max Brunsfeld
e08d6cd6de Merge pull request #1921 from zed-industries/multibuffer-following
Allow following collaborators into editors with multi-excerpt buffers (refactors + find-all-refs)
2022-12-14 15:33:11 -08:00
Max Brunsfeld
954c9ac3fd Add integration test coverage for following into multibuffers 2022-12-14 15:28:58 -08:00
Max Brunsfeld
e4c5dfcf6c Use run_until_parked instead of 'condition' in all integration tests 2022-12-14 15:05:35 -08:00
Nate Butler
5f6313d336 Update Zed & Zed Preview icons 2022-12-14 17:41:18 -05:00
Max Brunsfeld
70efd2bebe Introduce a ViewId message, identifying views across calls 2022-12-14 14:40:07 -08:00
Max Brunsfeld
43b7e16c89 Handle retina screens in start-local-collaboration script 2022-12-14 11:50:15 -08:00
Max Brunsfeld
f99f581bfc Clean up state matching in from_state_proto using let/else statements 2022-12-14 11:09:33 -08:00
Max Brunsfeld
09d3fbf04f In editor following test, apply excerpt removals to both followers 2022-12-14 11:08:08 -08:00
Antonio Scandurra
363e3cae4b WIP 2022-12-14 19:25:07 +01:00
Antonio Scandurra
930be6706f WIP 2022-12-14 18:02:39 +01:00
Antonio Scandurra
05e99eb67e Introduce an epoch to ConnectionId and PeerId 2022-12-14 15:55:56 +01:00
Antonio Scandurra
9bd400cf16 collab 0.3.12 2022-12-14 11:43:33 +01:00
Antonio Scandurra
553585b9a1 Add more logging to Room 2022-12-14 11:43:12 +01:00
Antonio Scandurra
674fddac87 Instrument rpc::Server::start and reduce cleanup timeout again 2022-12-14 11:42:12 +01:00
Antonio Scandurra
63e7b9189d collab 0.3.11 2022-12-14 11:25:04 +01:00
Antonio Scandurra
9530976f61 Try using a longer timeout for cleaning up stale rooms 2022-12-14 11:24:36 +01:00
Antonio Scandurra
02c30b0091 collab 0.3.10 2022-12-14 09:35:52 +01:00
Antonio Scandurra
b9c7796547 Reduce readiness probe delay and period 2022-12-14 09:35:36 +01:00
Antonio Scandurra
e00cb6b074 collab 0.3.9 2022-12-14 09:05:19 +01:00
Antonio Scandurra
dc47552180 Fix kubernetes configuration for readiness probe 2022-12-14 08:58:19 +01:00
Antonio Scandurra
98a593b263 collab 0.3.8 2022-12-14 08:56:02 +01:00
Antonio Scandurra
897506c797 Define readiness probe to know when the new server can accept traffic 2022-12-14 08:54:46 +01:00
Antonio Scandurra
59c9a57570 collab 0.3.7 2022-12-14 08:43:18 +01:00
Antonio Scandurra
dde6cf596e Don't wait for stale project deletion before listening for connections 2022-12-14 08:42:34 +01:00
Antonio Scandurra
2596fefa04 collab 0.3.6 2022-12-13 23:09:02 +01:00
Antonio Scandurra
34b69896e4 Listen to SIGTERM in addition to ctrl-c for graceful shutdown 2022-12-13 23:08:43 +01:00
Antonio Scandurra
7824ace58b collab 0.3.5 2022-12-13 22:40:55 +01:00
Antonio Scandurra
b150efbd96 Set log level to debug for preview deployment
Also, add a log statement when we receive the interrupt signal.

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-13 22:38:55 +01:00
Julia
cf72173282 Clamp end of visual git hunk to requested range 2022-12-13 13:58:50 -05:00
Julia
ecd44e6914 Git diff recalc in project diagnostics 2022-12-13 12:35:58 -05:00
Julia
2cd9987b54 Git diff recalc in project search 2022-12-13 12:35:58 -05:00
Julia
7c3dc1e3dc Cleanup 2022-12-13 12:35:58 -05:00
Julia
00b7c78e33 Initial hacky displaying of git gutter in multi-buffers 2022-12-13 12:35:58 -05:00
Max Brunsfeld
11800a8a78 Merge branch 'main' into multibuffer-following 2022-12-13 09:25:18 -08:00
Max Brunsfeld
f797dfb88f Merge branch 'main' into multibuffer-following 2022-12-12 11:47:39 -08:00
Joseph Lyons
7608875625 Remove extraneous newline 2022-12-10 09:56:42 -05:00
Joseph Lyons
dcf11ac7e5 Add symlink to applications directory in dmg 2022-12-10 09:48:40 -05:00
Max Brunsfeld
82824f78b6 Make each Zed instance use half the screen in 'start-local-collaboration' script 2022-12-01 16:43:39 -08:00
Max Brunsfeld
e4507c1d74 Fetch missing buffers when adding excerpts to a multibuffer while following
Make FollowableItem::apply_update_proto asynchronous. Use a single
task per workspace to process all leader updates, to prevent updates
from being interleaved.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-12-01 15:17:51 -08:00
Max Brunsfeld
9314c0e313 Replicate multibuffer excerpt additions and removals to followers 2022-11-30 13:20:13 -08:00
Max Brunsfeld
a48cd9125b Start-local-collaboration script: put peers' windows at different positions 2022-11-30 09:29:49 -08:00
Max Brunsfeld
6120d6488b Start work on following in multi-buffers 2022-11-29 14:50:43 -08:00
Max Brunsfeld
82abf31ef1 Add start-local-collaboration script 2022-11-29 14:50:12 -08:00
Max Brunsfeld
6d9b55a654 Send full multibuffer anchors to following peers 2022-11-28 18:00:38 -08:00
Max Brunsfeld
3eac3e20d5 Emit events from a multibuffer when adding/removing excerpts 2022-11-28 17:57:55 -08:00
73 changed files with 4076 additions and 1955 deletions

22
Cargo.lock generated
View File

@@ -823,6 +823,7 @@ dependencies = [
"futures 0.3.25",
"gpui",
"live_kit_client",
"log",
"media",
"postage",
"project",
@@ -1130,7 +1131,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.3.4"
version = "0.4.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -4806,6 +4807,24 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "recent_projects"
version = "0.1.0"
dependencies = [
"db",
"editor",
"fuzzy",
"gpui",
"language",
"ordered-float",
"picker",
"postage",
"settings",
"smol",
"text",
"workspace",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -8180,6 +8199,7 @@ dependencies = [
"project_panel",
"project_symbols",
"rand 0.8.5",
"recent_projects",
"regex",
"rpc",
"rsa",

View File

@@ -40,6 +40,7 @@ members = [
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/recent_projects",
"crates/rope",
"crates/rpc",
"crates/search",

View File

@@ -36,6 +36,7 @@
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
"alt-cmd-o": "recent_projects::Toggle",
"ctrl-`": "workspace::NewTerminal"
}
},

View File

@@ -21,6 +21,7 @@ test-support = [
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
log = "0.4"
live_kit_client = { path = "../live_kit_client" }
media = { path = "../media" }
project = { path = "../project" }

View File

@@ -39,6 +39,7 @@ pub struct LocalParticipant {
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

View File

@@ -3,7 +3,10 @@ use crate::{
IncomingCall,
};
use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
};
use collections::{BTreeMap, HashSet};
use futures::{FutureExt, StreamExt};
use gpui::{
@@ -13,17 +16,17 @@ use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUp
use postage::stream::Stream;
use project::Project;
use std::{mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt};
use util::{post_inc, ResultExt, TryFutureExt};
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ParticipantLocationChanged {
participant_id: PeerId,
participant_id: proto::PeerId,
},
RemoteVideoTracksChanged {
participant_id: PeerId,
participant_id: proto::PeerId,
},
RemoteProjectShared {
owner: Arc<User>,
@@ -41,7 +44,7 @@ pub struct Room {
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
remote_participants: BTreeMap<u64, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
@@ -50,7 +53,7 @@ pub struct Room {
user_store: ModelHandle<UserStore>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Result<()>>>,
maintain_connection: Option<Task<Option<()>>>,
}
impl Entity for Room {
@@ -58,6 +61,7 @@ impl Entity for Room {
fn release(&mut self, _: &mut MutableAppContext) {
if self.status.is_online() {
log::info!("room was released, sending leave message");
self.client.send(proto::LeaveRoom {}).log_err();
}
}
@@ -122,7 +126,7 @@ impl Room {
};
let maintain_connection =
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx));
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
Self {
id,
@@ -229,6 +233,7 @@ impl Room {
cx.notify();
cx.emit(Event::Left);
log::info!("leaving room");
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
@@ -254,6 +259,7 @@ impl Room {
.map_or(false, |s| s.is_connected());
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || client_status.next().await.is_some() {
log::info!("detected client disconnection");
let room_id = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
@@ -269,8 +275,13 @@ impl Room {
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
if let Some(status) = client_status.next().await {
if status.is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let rejoin_room = async {
let response =
client.request(proto::JoinRoom { id: room_id }).await?;
@@ -285,7 +296,7 @@ impl Room {
anyhow::Ok(())
};
if rejoin_room.await.is_ok() {
if rejoin_room.await.log_err().is_some() {
return true;
} else {
remaining_attempts -= 1;
@@ -303,12 +314,15 @@ impl Room {
futures::select_biased! {
reconnected = client_reconnection => {
if reconnected {
log::info!("successfully reconnected to room");
// If we successfully joined the room, go back around the loop
// waiting for future connection status changes.
continue;
}
}
_ = reconnection_timeout => {}
_ = reconnection_timeout => {
log::info!("room reconnection timeout expired");
}
}
}
@@ -316,6 +330,7 @@ impl Room {
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
return Err(anyhow!(
@@ -337,10 +352,16 @@ impl Room {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
&self.remote_participants
}
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants
.values()
.find(|p| p.peer_id == peer_id)
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
@@ -405,15 +426,13 @@ impl Room {
}
if let Some(participants) = remote_participants.log_err() {
let mut participant_peer_ids = HashSet::default();
for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id);
let Some(peer_id) = participant.peer_id else { continue };
this.participant_user_ids.insert(participant.user_id);
participant_peer_ids.insert(peer_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.get(&participant.user_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
@@ -442,9 +461,11 @@ impl Room {
let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -453,9 +474,10 @@ impl Room {
}
} else {
this.remote_participants.insert(
peer_id,
participant.user_id,
RemoteParticipant {
user: user.clone(),
peer_id,
projects: participant.projects,
location,
tracks: Default::default(),
@@ -464,7 +486,7 @@ impl Room {
if let Some(live_kit) = this.live_kit.as_ref() {
let tracks =
live_kit.room.remote_video_tracks(&peer_id.0.to_string());
live_kit.room.remote_video_tracks(&peer_id.to_string());
for track in tracks {
this.remote_video_track_updated(
RemoteVideoTrackUpdate::Subscribed(track),
@@ -476,8 +498,8 @@ impl Room {
}
}
this.remote_participants.retain(|peer_id, participant| {
if participant_peer_ids.contains(peer_id) {
this.remote_participants.retain(|user_id, participant| {
if this.participant_user_ids.contains(user_id) {
true
} else {
for project in &participant.projects {
@@ -499,6 +521,7 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
let _ = this.leave(cx);
}
@@ -518,11 +541,11 @@ impl Room {
) -> Result<()> {
match change {
RemoteVideoTrackUpdate::Subscribed(track) => {
let peer_id = PeerId(track.publisher_id().parse()?);
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.tracks.insert(
track_id.clone(),
@@ -531,21 +554,21 @@ impl Room {
}),
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
RemoteVideoTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} => {
let peer_id = PeerId(publisher_id.parse()?);
let user_id = publisher_id.parse()?;
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
}

View File

@@ -23,7 +23,7 @@ use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize;
use std::{
any::TypeId,
@@ -140,7 +140,7 @@ impl EstablishConnectionError {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Status {
SignedOut,
UpgradeRequired,
@@ -306,7 +306,7 @@ impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
id: 0,
peer: Peer::new(),
peer: Peer::new(0),
telemetry: Telemetry::new(http.clone(), cx),
http,
state: Default::default(),
@@ -333,14 +333,14 @@ impl Client {
}
#[cfg(any(test, feature = "test-support"))]
pub fn tear_down(&self) {
pub fn teardown(&self) {
let mut state = self.state.write();
state._reconnect_task.take();
state.message_handlers.clear();
state.models_by_message_type.clear();
state.entities_by_type_and_remote_id.clear();
state.entity_id_extractors.clear();
self.peer.reset();
self.peer.teardown();
}
#[cfg(any(test, feature = "test-support"))]
@@ -810,7 +810,11 @@ impl Client {
hello_message_type_name
)
})?;
Ok(PeerId(hello.payload.peer_id))
let peer_id = hello
.payload
.peer_id
.ok_or_else(|| anyhow!("invalid peer id"))?;
Ok(peer_id)
};
let peer_id = match peer_id.await {
@@ -822,7 +826,7 @@ impl Client {
};
log::info!(
"set status to connected (connection id: {}, peer id: {})",
"set status to connected (connection id: {:?}, peer id: {:?})",
connection_id,
peer_id
);
@@ -853,7 +857,7 @@ impl Client {
.spawn(async move {
match handle_io.await {
Ok(()) => {
if *this.status().borrow()
if this.status().borrow().clone()
== (Status::Connected {
connection_id,
peer_id,
@@ -1194,7 +1198,7 @@ impl Client {
let mut state = self.state.write();
let type_name = message.payload_type_name();
let payload_type_id = message.payload_type_id();
let sender_id = message.original_sender_id().map(|id| id.0);
let sender_id = message.original_sender_id();
let mut subscriber = None;

View File

@@ -35,7 +35,7 @@ impl FakeServer {
cx: &TestAppContext,
) -> Self {
let server = Self {
peer: Peer::new(),
peer: Peer::new(0),
state: Default::default(),
user_id: client_user_id,
executor: cx.foreground(),
@@ -92,7 +92,7 @@ impl FakeServer {
peer.send(
connection_id,
proto::Hello {
peer_id: connection_id.0,
peer_id: Some(connection_id.into()),
},
)
.unwrap();

View File

@@ -2,6 +2,7 @@ DATABASE_URL = "postgres://postgres@localhost/zed"
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
ZED_ENVIRONMENT = "development"
LIVE_KIT_SERVER = "http://localhost:7880"
LIVE_KIT_KEY = "devkey"
LIVE_KIT_SECRET = "secret"

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.3.4"
version = "0.4.2"
[[bin]]
name = "collab"

View File

@@ -59,6 +59,12 @@ spec:
ports:
- containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 1
periodSeconds: 1
env:
- name: HTTP_PORT
value: "8080"
@@ -93,6 +99,8 @@ spec:
value: ${RUST_LOG}
- name: LOG_JSON
value: "true"
- name: ZED_ENVIRONMENT
value: ${ZED_ENVIRONMENT}
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.

View File

@@ -43,11 +43,12 @@ CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"host_connection_id" INTEGER NOT NULL,
"host_connection_epoch" TEXT NOT NULL,
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
CREATE TABLE "worktrees" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
@@ -103,34 +104,39 @@ CREATE TABLE "project_collaborators" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"connection_id" INTEGER NOT NULL,
"connection_epoch" TEXT NOT NULL,
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
"is_host" BOOLEAN NOT NULL
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
CREATE INDEX "index_project_collaborators_on_connection_epoch" ON "project_collaborators" ("connection_epoch");
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_epoch" ON "project_collaborators" ("project_id", "connection_id", "connection_epoch");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
CREATE TABLE "room_participants" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
"user_id" INTEGER NOT NULL REFERENCES users (id),
"answering_connection_id" INTEGER,
"answering_connection_epoch" TEXT,
"answering_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"answering_connection_lost" BOOLEAN NOT NULL,
"location_kind" INTEGER,
"location_project_id" INTEGER,
"initial_project_id" INTEGER,
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
"calling_connection_id" INTEGER NOT NULL,
"calling_connection_epoch" TEXT NOT NULL
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
CREATE INDEX "index_room_participants_on_answering_connection_epoch" ON "room_participants" ("answering_connection_epoch");
CREATE INDEX "index_room_participants_on_calling_connection_epoch" ON "room_participants" ("calling_connection_epoch");
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_epoch" ON "room_participants" ("answering_connection_id", "answering_connection_epoch");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");
CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"environment" VARCHAR NOT NULL
);

View File

@@ -0,0 +1,30 @@
CREATE TABLE servers (
id SERIAL PRIMARY KEY,
environment VARCHAR NOT NULL
);
DROP TABLE worktree_extensions;
DROP TABLE project_activity_periods;
DELETE from projects;
ALTER TABLE projects
DROP COLUMN host_connection_epoch,
ADD COLUMN host_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE;
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
DELETE FROM project_collaborators;
ALTER TABLE project_collaborators
DROP COLUMN connection_epoch,
ADD COLUMN connection_server_id INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE;
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
DELETE FROM room_participants;
ALTER TABLE room_participants
DROP COLUMN answering_connection_epoch,
DROP COLUMN calling_connection_epoch,
ADD COLUMN answering_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE,
ADD COLUMN calling_connection_server_id INTEGER REFERENCES servers (id) ON DELETE SET NULL;
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");

View File

@@ -5,6 +5,7 @@ mod project;
mod project_collaborator;
mod room;
mod room_participant;
mod server;
mod signup;
#[cfg(test)]
mod tests;
@@ -48,7 +49,6 @@ pub struct Database {
background: Option<std::sync::Arc<gpui::executor::Background>>,
#[cfg(test)]
runtime: Option<tokio::runtime::Runtime>,
epoch: parking_lot::RwLock<Uuid>,
}
impl Database {
@@ -61,18 +61,12 @@ impl Database {
background: None,
#[cfg(test)]
runtime: None,
epoch: parking_lot::RwLock::new(Uuid::new_v4()),
})
}
#[cfg(test)]
pub fn reset(&self) {
self.rooms.clear();
*self.epoch.write() = Uuid::new_v4();
}
fn epoch(&self) -> Uuid {
*self.epoch.read()
}
pub async fn migrate(
@@ -116,14 +110,40 @@ impl Database {
Ok(new_migrations)
}
pub async fn delete_stale_projects(&self) -> Result<()> {
pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
self.transaction(|tx| async move {
let server = server::ActiveModel {
environment: ActiveValue::set(environment.into()),
..Default::default()
}
.insert(&*tx)
.await?;
Ok(server.id)
})
.await
}
pub async fn delete_stale_projects(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<()> {
self.transaction(|tx| async move {
let stale_server_epochs = self
.stale_server_ids(environment, new_server_id, &tx)
.await?;
project_collaborator::Entity::delete_many()
.filter(project_collaborator::Column::ConnectionEpoch.ne(self.epoch()))
.filter(
project_collaborator::Column::ConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.exec(&*tx)
.await?;
project::Entity::delete_many()
.filter(project::Column::HostConnectionEpoch.ne(self.epoch()))
.filter(
project::Column::HostConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.exec(&*tx)
.await?;
Ok(())
@@ -131,18 +151,28 @@ impl Database {
.await
}
pub async fn stale_room_ids(&self) -> Result<Vec<RoomId>> {
pub async fn stale_room_ids(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<Vec<RoomId>> {
self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
RoomId,
}
let stale_server_epochs = self
.stale_server_ids(environment, new_server_id, &tx)
.await?;
Ok(room_participant::Entity::find()
.select_only()
.column(room_participant::Column::RoomId)
.distinct()
.filter(room_participant::Column::AnsweringConnectionEpoch.ne(self.epoch()))
.filter(
room_participant::Column::AnsweringConnectionServerId
.is_in(stale_server_epochs),
)
.into_values::<_, QueryAs>()
.all(&*tx)
.await?)
@@ -150,12 +180,16 @@ impl Database {
.await
}
pub async fn refresh_room(&self, room_id: RoomId) -> Result<RoomGuard<RefreshedRoom>> {
pub async fn refresh_room(
&self,
room_id: RoomId,
new_server_id: ServerId,
) -> Result<RoomGuard<RefreshedRoom>> {
self.room_transaction(|tx| async move {
let stale_participant_filter = Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
.add(room_participant::Column::AnsweringConnectionEpoch.ne(self.epoch()));
.add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id));
let stale_participant_user_ids = room_participant::Entity::find()
.filter(stale_participant_filter.clone())
@@ -199,6 +233,42 @@ impl Database {
.await
}
pub async fn delete_stale_servers(
&self,
new_server_id: ServerId,
environment: &str,
) -> Result<()> {
self.transaction(|tx| async move {
server::Entity::delete_many()
.filter(
Condition::all()
.add(server::Column::Environment.eq(environment))
.add(server::Column::Id.ne(new_server_id)),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
async fn stale_server_ids(
&self,
environment: &str,
new_server_id: ServerId,
tx: &DatabaseTransaction,
) -> Result<Vec<ServerId>> {
let stale_servers = server::Entity::find()
.filter(
Condition::all()
.add(server::Column::Environment.eq(environment))
.add(server::Column::Id.ne(new_server_id)),
)
.all(&*tx)
.await?;
Ok(stale_servers.into_iter().map(|server| server.id).collect())
}
// users
pub async fn create_user(
@@ -1076,7 +1146,7 @@ impl Database {
pub async fn create_room(
&self,
user_id: UserId,
connection_id: ConnectionId,
connection: ConnectionId,
live_kit_room: &str,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
@@ -1091,12 +1161,16 @@ impl Database {
room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(user_id),
answering_connection_id: ActiveValue::set(Some(connection_id.0 as i32)),
answering_connection_epoch: ActiveValue::set(Some(self.epoch())),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
calling_user_id: ActiveValue::set(user_id),
calling_connection_id: ActiveValue::set(connection_id.0 as i32),
calling_connection_epoch: ActiveValue::set(self.epoch()),
calling_connection_id: ActiveValue::set(connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..Default::default()
}
.insert(&*tx)
@@ -1112,7 +1186,7 @@ impl Database {
&self,
room_id: RoomId,
calling_user_id: UserId,
calling_connection_id: ConnectionId,
calling_connection: ConnectionId,
called_user_id: UserId,
initial_project_id: Option<ProjectId>,
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
@@ -1122,8 +1196,10 @@ impl Database {
user_id: ActiveValue::set(called_user_id),
answering_connection_lost: ActiveValue::set(false),
calling_user_id: ActiveValue::set(calling_user_id),
calling_connection_id: ActiveValue::set(calling_connection_id.0 as i32),
calling_connection_epoch: ActiveValue::set(self.epoch()),
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
calling_connection.owner_id as i32,
))),
initial_project_id: ActiveValue::set(initial_project_id),
..Default::default()
}
@@ -1162,57 +1238,64 @@ impl Database {
&self,
expected_room_id: Option<RoomId>,
user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(
room_participant::Column::UserId
.eq(user_id)
.and(room_participant::Column::AnsweringConnectionId.is_null()),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("could not decline call"))?;
let room_id = participant.room_id;
if expected_room_id.map_or(false, |expected_room_id| expected_room_id != room_id) {
return Err(anyhow!("declining call on unexpected room"))?;
) -> Result<Option<RoomGuard<proto::Room>>> {
self.optional_room_transaction(|tx| async move {
let mut filter = Condition::all()
.add(room_participant::Column::UserId.eq(user_id))
.add(room_participant::Column::AnsweringConnectionId.is_null());
if let Some(room_id) = expected_room_id {
filter = filter.add(room_participant::Column::RoomId.eq(room_id));
}
let participant = room_participant::Entity::find()
.filter(filter)
.one(&*tx)
.await?;
let participant = if let Some(participant) = participant {
participant
} else if expected_room_id.is_some() {
return Err(anyhow!("could not find call to decline"))?;
} else {
return Ok(None);
};
let room_id = participant.room_id;
room_participant::Entity::delete(participant.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(Some((room_id, room)))
})
.await
}
pub async fn cancel_call(
&self,
expected_room_id: Option<RoomId>,
calling_connection_id: ConnectionId,
room_id: RoomId,
calling_connection: ConnectionId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(
room_participant::Column::UserId
.eq(called_user_id)
.and(
Condition::all()
.add(room_participant::Column::UserId.eq(called_user_id))
.add(room_participant::Column::RoomId.eq(room_id))
.add(
room_participant::Column::CallingConnectionId
.eq(calling_connection_id.0 as i32),
.eq(calling_connection.id as i32),
)
.and(room_participant::Column::AnsweringConnectionId.is_null()),
.add(
room_participant::Column::CallingConnectionServerId
.eq(calling_connection.owner_id as i32),
)
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("could not cancel call"))?;
.ok_or_else(|| anyhow!("no call to cancel"))?;
let room_id = participant.room_id;
if expected_room_id.map_or(false, |expected_room_id| expected_room_id != room_id) {
return Err(anyhow!("canceling call on unexpected room"))?;
}
room_participant::Entity::delete(participant.into_active_model())
.exec(&*tx)
@@ -1228,7 +1311,7 @@ impl Database {
&self,
room_id: RoomId,
user_id: UserId,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let result = room_participant::Entity::update_many()
@@ -1241,14 +1324,16 @@ impl Database {
.add(room_participant::Column::AnsweringConnectionId.is_null())
.add(room_participant::Column::AnsweringConnectionLost.eq(true))
.add(
room_participant::Column::AnsweringConnectionEpoch
.ne(self.epoch()),
room_participant::Column::AnsweringConnectionServerId
.ne(connection.owner_id as i32),
),
),
)
.set(room_participant::ActiveModel {
answering_connection_id: ActiveValue::set(Some(connection_id.0 as i32)),
answering_connection_epoch: ActiveValue::set(Some(self.epoch())),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
..Default::default()
})
@@ -1264,10 +1349,23 @@ impl Database {
.await
}
pub async fn leave_room(&self, connection_id: ConnectionId) -> Result<RoomGuard<LeftRoom>> {
self.room_transaction(|tx| async move {
pub async fn leave_room(
&self,
connection: ConnectionId,
) -> Result<Option<RoomGuard<LeftRoom>>> {
self.optional_room_transaction(|tx| async move {
let leaving_participant = room_participant::Entity::find()
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?;
@@ -1281,9 +1379,16 @@ impl Database {
// Cancel pending calls initiated by the leaving user.
let called_participants = room_participant::Entity::find()
.filter(
room_participant::Column::CallingConnectionId
.eq(connection_id.0)
.and(room_participant::Column::AnsweringConnectionId.is_null()),
Condition::all()
.add(
room_participant::Column::CallingConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::CallingConnectionServerId
.eq(connection.owner_id as i32),
)
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.all(&*tx)
.await?;
@@ -1310,7 +1415,16 @@ impl Database {
project_collaborator::Column::ProjectId,
QueryProjectIds::ProjectId,
)
.filter(project_collaborator::Column::ConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
project_collaborator::Column::ConnectionId.eq(connection.id as i32),
)
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.into_values::<_, QueryProjectIds>()
.all(&*tx)
.await?;
@@ -1331,32 +1445,46 @@ impl Database {
host_connection_id: Default::default(),
});
let collaborator_connection_id =
ConnectionId(collaborator.connection_id as u32);
if collaborator_connection_id != connection_id {
let collaborator_connection_id = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
if collaborator_connection_id != connection {
left_project.connection_ids.push(collaborator_connection_id);
}
if collaborator.is_host {
left_project.host_user_id = collaborator.user_id;
left_project.host_connection_id =
ConnectionId(collaborator.connection_id as u32);
left_project.host_connection_id = collaborator_connection_id;
}
}
drop(collaborators);
// Leave projects.
project_collaborator::Entity::delete_many()
.filter(project_collaborator::Column::ConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
project_collaborator::Column::ConnectionId.eq(connection.id as i32),
)
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
// Unshare projects.
project::Entity::delete_many()
.filter(
project::Column::RoomId
.eq(room_id)
.and(project::Column::HostConnectionId.eq(connection_id.0 as i32)),
Condition::all()
.add(project::Column::RoomId.eq(room_id))
.add(project::Column::HostConnectionId.eq(connection.id as i32))
.add(
project::Column::HostConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
@@ -1376,9 +1504,9 @@ impl Database {
self.rooms.remove(&room_id);
}
Ok((room_id, left_room))
Ok(Some((room_id, left_room)))
} else {
Err(anyhow!("could not leave room"))?
Ok(None)
}
})
.await
@@ -1387,7 +1515,7 @@ impl Database {
pub async fn update_room_participant_location(
&self,
room_id: RoomId,
connection_id: ConnectionId,
connection: ConnectionId,
location: proto::ParticipantLocation,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async {
@@ -1414,9 +1542,18 @@ impl Database {
}
let result = room_participant::Entity::update_many()
.filter(room_participant::Column::RoomId.eq(room_id).and(
room_participant::Column::AnsweringConnectionId.eq(connection_id.0 as i32),
))
.filter(
Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
)
.set(room_participant::ActiveModel {
location_kind: ActiveValue::set(Some(location_kind)),
location_project_id: ActiveValue::set(location_project_id),
@@ -1437,11 +1574,21 @@ impl Database {
pub async fn connection_lost(
&self,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<LeftProject>>> {
self.room_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("not a participant in any room"))?;
@@ -1456,11 +1603,25 @@ impl Database {
let collaborator_on_projects = project_collaborator::Entity::find()
.find_also_related(project::Entity)
.filter(project_collaborator::Column::ConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.all(&*tx)
.await?;
project_collaborator::Entity::delete_many()
.filter(project_collaborator::Column::ConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
@@ -1473,20 +1634,29 @@ impl Database {
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| ConnectionId(collaborator.connection_id as u32))
.map(|collaborator| ConnectionId {
id: collaborator.connection_id as u32,
owner_id: collaborator.connection_server_id.0 as u32,
})
.collect();
left_projects.push(LeftProject {
id: project.id,
host_user_id: project.host_user_id,
host_connection_id: ConnectionId(project.host_connection_id as u32),
host_connection_id: project.host_connection()?,
connection_ids,
});
}
}
project::Entity::delete_many()
.filter(project::Column::HostConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
.add(
project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
@@ -1537,7 +1707,10 @@ impl Database {
let mut pending_participants = Vec::new();
while let Some(db_participant) = db_participants.next().await {
let db_participant = db_participant?;
if let Some(answering_connection_id) = db_participant.answering_connection_id {
if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
.answering_connection_id
.zip(db_participant.answering_connection_server_id)
{
let location = match (
db_participant.location_kind,
db_participant.location_project_id,
@@ -1556,11 +1729,16 @@ impl Database {
Default::default(),
)),
};
let answering_connection = ConnectionId {
owner_id: answering_connection_server_id.0 as u32,
id: answering_connection_id as u32,
};
participants.insert(
answering_connection_id,
answering_connection,
proto::Participant {
user_id: db_participant.user_id.to_proto(),
peer_id: answering_connection_id as u32,
peer_id: Some(answering_connection.into()),
projects: Default::default(),
location: Some(proto::ParticipantLocation { variant: location }),
},
@@ -1583,7 +1761,8 @@ impl Database {
while let Some(row) = db_projects.next().await {
let (db_project, db_worktree) = row?;
if let Some(participant) = participants.get_mut(&db_project.host_connection_id) {
let host_connection = db_project.host_connection()?;
if let Some(participant) = participants.get_mut(&host_connection) {
let project = if let Some(project) = participant
.projects
.iter_mut()
@@ -1637,12 +1816,22 @@ impl Database {
pub async fn share_project(
&self,
room_id: RoomId,
connection_id: ConnectionId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
self.room_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("could not find participant"))?;
@@ -1653,8 +1842,10 @@ impl Database {
let project = project::ActiveModel {
room_id: ActiveValue::set(participant.room_id),
host_user_id: ActiveValue::set(participant.user_id),
host_connection_id: ActiveValue::set(connection_id.0 as i32),
host_connection_epoch: ActiveValue::set(self.epoch()),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..Default::default()
}
.insert(&*tx)
@@ -1678,8 +1869,8 @@ impl Database {
project_collaborator::ActiveModel {
project_id: ActiveValue::set(project.id),
connection_id: ActiveValue::set(connection_id.0 as i32),
connection_epoch: ActiveValue::set(self.epoch()),
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
user_id: ActiveValue::set(participant.user_id),
replica_id: ActiveValue::set(ReplicaId(0)),
is_host: ActiveValue::set(true),
@@ -1697,7 +1888,7 @@ impl Database {
pub async fn unshare_project(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
@@ -1706,7 +1897,7 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project not found"))?;
if project.host_connection_id == connection_id.0 as i32 {
if project.host_connection()? == connection {
let room_id = project.room_id;
project::Entity::delete(project.into_active_model())
.exec(&*tx)
@@ -1723,12 +1914,18 @@ impl Database {
pub async fn update_project(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.filter(project::Column::HostConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
.add(
project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
@@ -1774,7 +1971,7 @@ impl Database {
pub async fn update_worktree(
&self,
update: &proto::UpdateWorktree,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
@@ -1782,7 +1979,13 @@ impl Database {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.filter(project::Column::HostConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
.add(
project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
@@ -1862,7 +2065,7 @@ impl Database {
pub async fn update_diagnostic_summary(
&self,
update: &proto::UpdateDiagnosticSummary,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
@@ -1877,7 +2080,7 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id != connection_id.0 as i32 {
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
@@ -1916,7 +2119,7 @@ impl Database {
pub async fn start_language_server(
&self,
update: &proto::StartLanguageServer,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
@@ -1930,7 +2133,7 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id != connection_id.0 as i32 {
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
@@ -1961,11 +2164,21 @@ impl Database {
pub async fn join_project(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<(Project, ReplicaId)>> {
self.room_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.0 as i32))
.filter(
Condition::all()
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("must join a room first"))?;
@@ -1992,8 +2205,8 @@ impl Database {
}
let new_collaborator = project_collaborator::ActiveModel {
project_id: ActiveValue::set(project_id),
connection_id: ActiveValue::set(connection_id.0 as i32),
connection_epoch: ActiveValue::set(self.epoch()),
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
user_id: ActiveValue::set(participant.user_id),
replica_id: ActiveValue::set(replica_id),
is_host: ActiveValue::set(false),
@@ -2095,14 +2308,18 @@ impl Database {
pub async fn leave_project(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<LeftProject>> {
self.room_transaction(|tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
project_collaborator::Column::ProjectId
.eq(project_id)
.and(project_collaborator::Column::ConnectionId.eq(connection_id.0 as i32)),
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
@@ -2120,13 +2337,16 @@ impl Database {
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| ConnectionId(collaborator.connection_id as u32))
.map(|collaborator| ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
})
.collect();
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
host_connection_id: ConnectionId(project.host_connection_id as u32),
host_connection_id: project.host_connection()?,
connection_ids,
};
Ok((project.room_id, left_project))
@@ -2137,7 +2357,7 @@ impl Database {
pub async fn project_collaborators(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<project_collaborator::Model>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
@@ -2149,10 +2369,13 @@ impl Database {
.all(&*tx)
.await?;
if collaborators
.iter()
.any(|collaborator| collaborator.connection_id == connection_id.0 as i32)
{
if collaborators.iter().any(|collaborator| {
let collaborator_connection = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
collaborator_connection == connection
}) {
Ok((project.room_id, collaborators))
} else {
Err(anyhow!("no such project"))?
@@ -2167,29 +2390,22 @@ impl Database {
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
self.room_transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
ConnectionId,
}
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let mut db_connection_ids = project_collaborator::Entity::find()
.select_only()
.column_as(
project_collaborator::Column::ConnectionId,
QueryAs::ConnectionId,
)
let mut participants = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.into_values::<i32, QueryAs>()
.stream(&*tx)
.await?;
let mut connection_ids = HashSet::default();
while let Some(connection_id) = db_connection_ids.next().await {
connection_ids.insert(ConnectionId(connection_id? as u32));
while let Some(participant) = participants.next().await {
let participant = participant?;
connection_ids.insert(ConnectionId {
owner_id: participant.connection_server_id.0 as u32,
id: participant.connection_id as u32,
});
}
if connection_ids.contains(&connection_id) {
@@ -2206,29 +2422,22 @@ impl Database {
project_id: ProjectId,
tx: &DatabaseTransaction,
) -> Result<Vec<ConnectionId>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
ConnectionId,
}
let mut db_guest_connection_ids = project_collaborator::Entity::find()
.select_only()
.column_as(
project_collaborator::Column::ConnectionId,
QueryAs::ConnectionId,
)
let mut participants = project_collaborator::Entity::find()
.filter(
project_collaborator::Column::ProjectId
.eq(project_id)
.and(project_collaborator::Column::IsHost.eq(false)),
)
.into_values::<i32, QueryAs>()
.stream(tx)
.await?;
let mut guest_connection_ids = Vec::new();
while let Some(connection_id) = db_guest_connection_ids.next().await {
guest_connection_ids.push(ConnectionId(connection_id? as u32));
while let Some(participant) = participants.next().await {
let participant = participant?;
guest_connection_ids.push(ConnectionId {
owner_id: participant.connection_server_id.0 as u32,
id: participant.connection_id as u32,
});
}
Ok(guest_connection_ids)
}
@@ -2327,25 +2536,25 @@ impl Database {
self.run(body).await
}
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<(RoomId, T)>>,
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
{
let body = async {
loop {
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok((room_id, data)) => {
Ok(Some((room_id, data))) => {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
return Ok(Some(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}));
}
Err(error) => {
if is_serialization_error(&error) {
@@ -2356,6 +2565,18 @@ impl Database {
}
}
}
Ok(None) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(None),
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
}
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
@@ -2371,6 +2592,23 @@ impl Database {
self.run(body).await
}
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<(RoomId, T)>>,
{
let data = self
.optional_room_transaction(move |tx| {
let future = f(tx);
async {
let data = future.await?;
Ok(Some(data))
}
})
.await?;
Ok(data.unwrap())
}
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
where
F: Send + Fn(TransactionHandle) -> Fut,
@@ -2607,6 +2845,7 @@ id_type!(RoomParticipantId);
id_type!(ProjectId);
id_type!(ProjectCollaboratorId);
id_type!(ReplicaId);
id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);

View File

@@ -1,4 +1,6 @@
use super::{ProjectId, RoomId, UserId};
use super::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -8,8 +10,23 @@ pub struct Model {
pub id: ProjectId,
pub room_id: RoomId,
pub host_user_id: UserId,
pub host_connection_id: i32,
pub host_connection_epoch: Uuid,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
}
impl Model {
pub fn host_connection(&self) -> Result<ConnectionId> {
let host_connection_server_id = self
.host_connection_server_id
.ok_or_else(|| anyhow!("empty host_connection_server_id"))?;
let host_connection_id = self
.host_connection_id
.ok_or_else(|| anyhow!("empty host_connection_id"))?;
Ok(ConnectionId {
owner_id: host_connection_server_id.0 as u32,
id: host_connection_id as u32,
})
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,4 +1,4 @@
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, UserId};
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -8,7 +8,7 @@ pub struct Model {
pub id: ProjectCollaboratorId,
pub project_id: ProjectId,
pub connection_id: i32,
pub connection_epoch: Uuid,
pub connection_server_id: ServerId,
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,

View File

@@ -1,4 +1,4 @@
use super::{ProjectId, RoomId, RoomParticipantId, UserId};
use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -9,14 +9,14 @@ pub struct Model {
pub room_id: RoomId,
pub user_id: UserId,
pub answering_connection_id: Option<i32>,
pub answering_connection_epoch: Option<Uuid>,
pub answering_connection_server_id: Option<ServerId>,
pub answering_connection_lost: bool,
pub location_kind: Option<i32>,
pub location_project_id: Option<ProjectId>,
pub initial_project_id: Option<ProjectId>,
pub calling_user_id: UserId,
pub calling_connection_id: i32,
pub calling_connection_epoch: Uuid,
pub calling_connection_server_id: Option<ServerId>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -0,0 +1,15 @@
use super::ServerId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ServerId,
pub environment: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -410,6 +410,8 @@ test_both_dbs!(
test_project_count_sqlite,
db,
{
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user1 = db
.create_user(
&format!("admin@example.com"),
@@ -436,36 +438,44 @@ test_both_dbs!(
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId(0), "")
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
.await
.unwrap()
.id,
);
db.call(room_id, user1.user_id, ConnectionId(0), user2.user_id, None)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId(1))
db.call(
room_id,
user1.user_id,
ConnectionId { owner_id, id: 0 },
user2.user_id,
None,
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId(1), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(room_id, ConnectionId(1), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
db.share_project(room_id, ConnectionId(0), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
db.leave_room(ConnectionId(1)).await.unwrap();
db.leave_room(ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
}
);

View File

@@ -7,13 +7,13 @@ use crate::{
use anyhow::anyhow;
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{
self, test::FakeHttpClient, Client, Connection, Credentials, EstablishConnectionError, PeerId,
User, UserStore, RECEIVE_TIMEOUT,
self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT,
};
use collections::{BTreeMap, HashMap, HashSet};
use editor::{
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
ToggleCodeActions, Undo,
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer,
Redo, Rename, ToOffset, ToggleCodeActions, Undo,
};
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
use futures::{channel::oneshot, StreamExt as _};
@@ -22,7 +22,7 @@ use gpui::{
TestAppContext, ViewHandle,
};
use language::{
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
range_to_lsp, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, PointUtf16, Rope,
};
use live_kit_client::MacOSDisplay;
@@ -199,7 +199,7 @@ async fn test_basic_calls(
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.peer_id().unwrap()]
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.len(),
1
@@ -492,7 +492,7 @@ async fn test_client_disconnecting_from_room(
// to automatically leave the room.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
deterministic.run_until_parked();
active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
@@ -608,7 +608,7 @@ async fn test_server_restarts(
);
// The server is torn down.
server.teardown();
server.reset().await;
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
@@ -778,7 +778,7 @@ async fn test_server_restarts(
);
// The server is torn down.
server.teardown();
server.reset().await;
// Users A and B have troubles reconnecting, so they leave the room.
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
@@ -1058,17 +1058,22 @@ async fn test_share_project(
let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// TODO
// // Create a selection set as client B and see that selection set as client A.
// buffer_a
// .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
// .await;
// Client A sees client B's selection
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
buffer
.snapshot()
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.count()
== 1
});
// Edit the buffer as client B and see that edit as client A.
editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
buffer_a
.condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents")
.await;
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "ok, b-contents")
});
// Client B can invite client C on a project shared by client A.
active_call_b
@@ -1091,12 +1096,16 @@ async fn test_share_project(
.build_remote_project(initial_project.id, cx_c)
.await;
// TODO
// // Remove the selection set as client B, see those selections disappear as client A.
// Client B closes the editor, and client A sees client B's selections removed.
cx_b.update(move |_| drop(editor_b));
// buffer_a
// .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
// .await;
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
buffer
.snapshot()
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.count()
== 0
});
}
#[gpui::test(iterations = 10)]
@@ -1250,13 +1259,9 @@ async fn test_host_disconnect(
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
project_a
.condition(cx_a, |project, _| project.collaborators().is_empty())
.await;
project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_b
.condition(cx_b, |project, _| project.is_read_only())
.await;
project_b.read_with(cx_b, |project, _| project.is_read_only());
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
// Ensure client B's edited state is reset and that the whole window is blurred.
@@ -1641,9 +1646,8 @@ async fn test_propagate_saves_and_fs_changes(
.await
.unwrap();
buffer_a
.condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
.await;
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
buffer_a.update(cx_a, |buf, cx| {
buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
});
@@ -1813,7 +1817,7 @@ async fn test_git_diff_base_change(
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1833,7 +1837,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1857,7 +1861,7 @@ async fn test_git_diff_base_change(
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1868,7 +1872,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1911,7 +1915,7 @@ async fn test_git_diff_base_change(
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1931,7 +1935,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1959,12 +1963,12 @@ async fn test_git_diff_base_change(
"{:?}",
buffer
.snapshot()
.git_diff_hunks_in_range(0..4, false)
.git_diff_hunks_in_row_range(0..4, false)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1975,7 +1979,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2297,9 +2301,8 @@ async fn test_buffer_conflict_after_save(
});
buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
buffer_b
.condition(cx_b, |buffer_b, _| !buffer_b.is_dirty())
.await;
cx_a.foreground().forbid_parking();
buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
buffer_b.read_with(cx_b, |buf, _| {
assert!(!buf.has_conflict());
});
@@ -2359,12 +2362,9 @@ async fn test_buffer_reloading(
.save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
.await
.unwrap();
buffer_b
.condition(cx_b, |buf, _| {
buf.text() == new_contents.to_string() && !buf.is_dirty()
})
.await;
cx_a.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buf, _| {
assert_eq!(buf.text(), new_contents.to_string());
assert!(!buf.is_dirty());
assert!(!buf.has_conflict());
assert_eq!(buf.line_ending(), LineEnding::Windows);
@@ -2416,7 +2416,8 @@ async fn test_editing_while_guest_opens_buffer(
let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
let buffer_b = buffer_b.await.unwrap();
buffer_b.condition(cx_b, |buf, _| buf.text() == text).await;
cx_a.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
}
#[gpui::test(iterations = 10)]
@@ -2446,9 +2447,8 @@ async fn test_leaving_worktree_while_opening_buffer(
let project_b = client_b.build_remote_project(project_id, cx_b).await;
// See that a guest has joined as client A.
project_a
.condition(cx_a, |p, _| p.collaborators().len() == 1)
.await;
cx_a.foreground().run_until_parked();
project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
// Begin opening a buffer as client B, but leave the project before the open completes.
let buffer_b = cx_b
@@ -2458,9 +2458,8 @@ async fn test_leaving_worktree_while_opening_buffer(
drop(buffer_b);
// See that the guest has left.
project_a
.condition(cx_a, |p, _| p.collaborators().is_empty())
.await;
cx_a.foreground().run_until_parked();
project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
}
#[gpui::test(iterations = 10)]
@@ -2979,9 +2978,10 @@ async fn test_collaborating_with_completion(
});
let fake_language_server = fake_language_servers.next().await.unwrap();
buffer_b
.condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
.await;
cx_a.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
assert!(!buffer.completion_triggers().is_empty())
});
// Type a completion trigger character as the guest.
editor_b.update(cx_b, |editor, cx| {
@@ -3043,14 +3043,13 @@ async fn test_collaborating_with_completion(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
buffer_a
.condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
.await;
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a. }")
});
// Confirm a completion on the guest.
editor_b
.condition(cx_b, |editor, _| editor.context_menu_visible())
.await;
editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
editor_b.update(cx_b, |editor, cx| {
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
@@ -3079,16 +3078,19 @@ async fn test_collaborating_with_completion(
);
// The additional edit is applied.
buffer_a
.condition(cx_a, |buffer, _| {
buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
})
.await;
buffer_b
.condition(cx_b, |buffer, _| {
buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
})
.await;
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(
buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method() }"
);
});
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(
buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method() }"
);
});
}
#[gpui::test(iterations = 10)]
@@ -3134,9 +3136,8 @@ async fn test_reloading_buffer_manually(
assert!(buffer.is_dirty());
assert!(!buffer.has_conflict());
});
buffer_a
.condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
.await;
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
client_a
.fs
@@ -3147,12 +3148,9 @@ async fn test_reloading_buffer_manually(
)
.await
.unwrap();
buffer_a
.condition(cx_a, |buffer, _| buffer.has_conflict())
.await;
buffer_b
.condition(cx_b, |buffer, _| buffer.has_conflict())
.await;
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
project_b
.update(cx_b, |project, cx| {
@@ -4178,9 +4176,8 @@ async fn test_collaborating_with_code_actions(
cx,
);
});
editor_b
.condition(cx_b, |editor, _| editor.context_menu_visible())
.await;
cx_a.foreground().run_until_parked();
editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
@@ -5162,9 +5159,9 @@ async fn test_following(
.insert_tree(
"/a",
json!({
"1.txt": "one",
"2.txt": "two",
"3.txt": "three",
"1.txt": "one\none\none",
"2.txt": "two\ntwo\ntwo",
"3.txt": "three\nthree\nthree",
}),
)
.await;
@@ -5263,11 +5260,60 @@ async fn test_following(
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx)
});
workspace_b
.condition(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b1.id()
})
.await;
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
});
// When client A opens a multibuffer, client B does so as well.
let multibuffer_a = cx_a.add_model(|cx| {
let buffer_a1 = project_a.update(cx, |project, cx| {
project
.get_open_buffer(&(worktree_id, "1.txt").into(), cx)
.unwrap()
});
let buffer_a2 = project_a.update(cx, |project, cx| {
project
.get_open_buffer(&(worktree_id, "2.txt").into(), cx)
.unwrap()
});
let mut result = MultiBuffer::new(0);
result.push_excerpts(
buffer_a1,
[ExcerptRange {
context: 0..3,
primary: None,
}],
cx,
);
result.push_excerpts(
buffer_a2,
[ExcerptRange {
context: 4..7,
primary: None,
}],
cx,
);
result
});
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
let editor =
cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
workspace.add_item(Box::new(editor.clone()), cx);
editor
});
deterministic.run_until_parked();
let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
);
// When client A navigates back and forth, client B does so as well.
workspace_a
@@ -5275,47 +5321,52 @@ async fn test_following(
workspace::Pane::go_back(workspace, None, cx)
})
.await;
workspace_b
.condition(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b2.id()
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
});
workspace_a
.update(cx_a, |workspace, cx| {
workspace::Pane::go_back(workspace, None, cx)
})
.await;
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
});
workspace_a
.update(cx_a, |workspace, cx| {
workspace::Pane::go_forward(workspace, None, cx)
})
.await;
workspace_b
.condition(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().id() == editor_b1.id()
})
.await;
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
});
// Changes to client A's editor are reflected on client B.
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
});
editor_b1
.condition(cx_b, |editor, cx| {
editor.selections.ranges(cx) == vec![1..1, 2..2]
})
.await;
deterministic.run_until_parked();
editor_b1.read_with(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
});
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
editor_b1
.condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
.await;
deterministic.run_until_parked();
editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(vec2f(0., 100.), cx);
});
editor_b1
.condition(cx_b, |editor, cx| {
editor.selections.ranges(cx) == vec![3..3]
})
.await;
deterministic.run_until_parked();
editor_b1.read_with(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), &[3..3]);
});
// After unfollowing, client B stops receiving updates from client A.
workspace_b.update(cx_b, |workspace, cx| {
@@ -5384,13 +5435,21 @@ async fn test_following(
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.id()),
editor_a1.id()
);
workspace_a.read_with(cx_a, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
});
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.activate_item(&multibuffer_editor_b, cx)
});
deterministic.run_until_parked();
workspace_a.read_with(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().id(),
multibuffer_editor_a.id()
)
});
// Client B activates an external window again, and the previously-opened screen-sharing item
// gets activated.
@@ -6066,7 +6125,7 @@ async fn test_random_collaboration(
.user_connection_ids(removed_guest_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let removed_peer_id = PeerId(user_connection_ids[0].0);
let removed_peer_id = user_connection_ids[0].into();
let guest = clients.remove(guest_ix);
op_start_signals.remove(guest_ix);
server.forbid_connections();
@@ -6115,17 +6174,25 @@ async fn test_random_collaboration(
.user_connection_ids(user_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let peer_id = PeerId(user_connection_ids[0].0);
let peer_id = user_connection_ids[0].into();
server.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
operations += 1;
}
30..=34 => {
log::info!("Simulating server restart");
server.teardown();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
server.reset().await;
deterministic.advance_clock(RECEIVE_TIMEOUT);
server.start().await.unwrap();
deterministic.advance_clock(CLEANUP_TIMEOUT);
let environment = &server.app_state.config.zed_environment;
let stale_room_ids = server
.app_state
.db
.stale_room_ids(environment, server.id())
.await
.unwrap();
assert_eq!(stale_room_ids, vec![]);
}
_ if !op_start_signals.is_empty() => {
while operations < max_operations && rng.lock().gen_bool(0.7) {
@@ -6320,7 +6387,13 @@ impl TestServer {
)
.unwrap();
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
let epoch = app_state
.db
.create_server(&app_state.config.zed_environment)
.await
.unwrap();
let server = Server::new(
epoch,
app_state.clone(),
Executor::Deterministic(deterministic.build_background()),
);
@@ -6337,9 +6410,15 @@ impl TestServer {
}
}
fn teardown(&self) {
self.server.teardown();
async fn reset(&self) {
self.app_state.db.reset();
let epoch = self
.app_state
.db
.create_server(&self.app_state.config.zed_environment)
.await
.unwrap();
self.server.reset(epoch);
}
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
@@ -6429,7 +6508,7 @@ impl TestServer {
let connection_id = connection_id_rx.await.unwrap();
connection_killers
.lock()
.insert(PeerId(connection_id.0), killed);
.insert(connection_id.into(), killed);
Ok(client_conn)
}
})
@@ -7251,7 +7330,7 @@ impl TestClient {
impl Drop for TestClient {
fn drop(&mut self) {
self.client.tear_down();
self.client.teardown();
}
}

View File

@@ -97,6 +97,7 @@ pub struct Config {
pub live_kit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
pub zed_environment: String,
}
#[derive(Default, Deserialize)]

View File

@@ -7,6 +7,7 @@ use std::{
net::{SocketAddr, TcpListener},
path::Path,
};
use tokio::signal::unix::SignalKind;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
@@ -56,7 +57,11 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = collab::rpc::Server::new(state.clone(), Executor::Production);
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
let app = collab::api::routes(rpc_server.clone(), state.clone())
@@ -66,9 +71,15 @@ async fn main() -> Result<()> {
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(async move {
tokio::signal::ctrl_c()
.await
let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())
.expect("failed to listen for interrupt signal");
let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt())
.expect("failed to listen for interrupt signal");
let sigterm = sigterm.recv();
let sigint = sigint.recv();
futures::pin_mut!(sigterm, sigint);
futures::future::select(sigterm, sigint).await;
tracing::info!("Received interrupt signal");
rpc_server.teardown();
})
.await?;

View File

@@ -2,7 +2,7 @@ mod connection_pool;
use crate::{
auth,
db::{self, Database, ProjectId, RoomId, User, UserId},
db::{self, Database, ProjectId, RoomId, ServerId, User, UserId},
executor::Executor,
AppState, Result,
};
@@ -138,6 +138,7 @@ impl Deref for DbHandle {
}
pub struct Server {
id: parking_lot::Mutex<ServerId>,
peer: Arc<Peer>,
pub(crate) connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
app_state: Arc<AppState>,
@@ -168,9 +169,10 @@ where
}
impl Server {
pub fn new(app_state: Arc<AppState>, executor: Executor) -> Arc<Self> {
pub fn new(id: ServerId, app_state: Arc<AppState>, executor: Executor) -> Arc<Self> {
let mut server = Self {
peer: Peer::new(),
id: parking_lot::Mutex::new(id),
peer: Peer::new(id.0 as u32),
app_state,
executor,
connection_pool: Default::default(),
@@ -239,97 +241,146 @@ impl Server {
}
pub async fn start(&self) -> Result<()> {
self.app_state.db.delete_stale_projects().await?;
let db = self.app_state.db.clone();
let server_id = *self.id.lock();
let app_state = self.app_state.clone();
let peer = self.peer.clone();
let timeout = self.executor.sleep(CLEANUP_TIMEOUT);
let pool = self.connection_pool.clone();
let live_kit_client = self.app_state.live_kit_client.clone();
self.executor.spawn_detached(async move {
timeout.await;
if let Some(room_ids) = db.stale_room_ids().await.trace_err() {
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Ok(mut refreshed_room) = db.refresh_room(room_id).await {
room_updated(&refreshed_room.room, &peer);
contacts_to_update
.extend(refreshed_room.stale_participant_user_ids.iter().copied());
contacts_to_update
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
}
let span = info_span!("start server");
let span_enter = span.enter();
{
let pool = pool.lock();
for canceled_user_id in canceled_calls_to_user_ids {
for connection_id in pool.user_connection_ids(canceled_user_id) {
peer.send(
connection_id,
proto::CallCanceled {
room_id: room_id.to_proto(),
},
)
.trace_err();
tracing::info!("begin deleting stale projects");
app_state
.db
.delete_stale_projects(&app_state.config.zed_environment, server_id)
.await?;
tracing::info!("finish deleting stale projects");
drop(span_enter);
self.executor.spawn_detached(
async move {
tracing::info!("waiting for cleanup timeout");
timeout.await;
tracing::info!("cleanup timeout expired, retrieving stale rooms");
if let Some(room_ids) = app_state
.db
.stale_room_ids(&app_state.config.zed_environment, server_id)
.await
.trace_err()
{
tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms");
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Ok(mut refreshed_room) =
app_state.db.refresh_room(room_id, server_id).await
{
tracing::info!(
room_id = room_id.0,
new_participant_count = refreshed_room.room.participants.len(),
"refreshed room"
);
room_updated(&refreshed_room.room, &peer);
contacts_to_update
.extend(refreshed_room.stale_participant_user_ids.iter().copied());
contacts_to_update
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
}
{
let pool = pool.lock();
for canceled_user_id in canceled_calls_to_user_ids {
for connection_id in pool.user_connection_ids(canceled_user_id) {
peer.send(
connection_id,
proto::CallCanceled {
room_id: room_id.to_proto(),
},
)
.trace_err();
}
}
}
}
for user_id in contacts_to_update {
let busy = db.is_user_busy(user_id).await.trace_err();
let contacts = db.get_contacts(user_id).await.trace_err();
if let Some((busy, contacts)) = busy.zip(contacts) {
let pool = pool.lock();
let updated_contact = contact_for_user(user_id, false, busy, &pool);
for contact in contacts {
if let db::Contact::Accepted {
user_id: contact_user_id,
..
} = contact
{
for contact_conn_id in pool.user_connection_ids(contact_user_id)
for user_id in contacts_to_update {
let busy = app_state.db.is_user_busy(user_id).await.trace_err();
let contacts = app_state.db.get_contacts(user_id).await.trace_err();
if let Some((busy, contacts)) = busy.zip(contacts) {
let pool = pool.lock();
let updated_contact = contact_for_user(user_id, false, busy, &pool);
for contact in contacts {
if let db::Contact::Accepted {
user_id: contact_user_id,
..
} = contact
{
peer.send(
contact_conn_id,
proto::UpdateContacts {
contacts: vec![updated_contact.clone()],
remove_contacts: Default::default(),
incoming_requests: Default::default(),
remove_incoming_requests: Default::default(),
outgoing_requests: Default::default(),
remove_outgoing_requests: Default::default(),
},
)
.trace_err();
for contact_conn_id in
pool.user_connection_ids(contact_user_id)
{
peer.send(
contact_conn_id,
proto::UpdateContacts {
contacts: vec![updated_contact.clone()],
remove_contacts: Default::default(),
incoming_requests: Default::default(),
remove_incoming_requests: Default::default(),
outgoing_requests: Default::default(),
remove_outgoing_requests: Default::default(),
},
)
.trace_err();
}
}
}
}
}
}
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
}
}
}
}
app_state
.db
.delete_stale_servers(server_id, &app_state.config.zed_environment)
.await
.trace_err();
}
});
.instrument(span),
);
Ok(())
}
pub fn teardown(&self) {
self.peer.reset();
self.peer.teardown();
self.connection_pool.lock().reset();
let _ = self.teardown.send(());
}
#[cfg(test)]
pub fn reset(&self, id: ServerId) {
self.teardown();
*self.id.lock() = id;
self.peer.reset(id.0 as u32);
}
#[cfg(test)]
pub fn id(&self) -> ServerId {
*self.id.lock()
}
fn add_handler<F, Fut, M>(&mut self, handler: F) -> &mut Self
where
F: 'static + Send + Sync + Fn(TypedEnvelope<M>, Session) -> Fut,
@@ -438,7 +489,7 @@ impl Server {
});
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
this.peer.send(connection_id, proto::Hello { peer_id: connection_id.0 })?;
this.peer.send(connection_id, proto::Hello { peer_id: Some(connection_id.into()) })?;
tracing::info!(%user_id, %login, %connection_id, %address, "sent hello message");
if let Some(send_connection_id) = send_connection_id.take() {
@@ -769,7 +820,7 @@ async fn sign_out(
.is_user_online(session.user_id)
{
let db = session.db().await;
if let Some(room) = db.decline_call(None, session.user_id).await.trace_err() {
if let Some(room) = db.decline_call(None, session.user_id).await.trace_err().flatten() {
room_updated(&room, &session.peer);
}
}
@@ -799,7 +850,7 @@ async fn create_room(
.trace_err()
{
if let Some(token) = live_kit
.room_token(&live_kit_room, &session.connection_id.to_string())
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -867,7 +918,7 @@ async fn join_room(
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(token) = live_kit
.room_token(&room.live_kit_room, &session.connection_id.to_string())
.room_token(&room.live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -973,7 +1024,7 @@ async fn cancel_call(
let room = session
.db()
.await
.cancel_call(Some(room_id), session.connection_id, called_user_id)
.cancel_call(room_id, session.connection_id, called_user_id)
.await?;
room_updated(&room, &session.peer);
}
@@ -1006,7 +1057,8 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<(
.db()
.await
.decline_call(Some(room_id), session.user_id)
.await?;
.await?
.ok_or_else(|| anyhow!("failed to decline call"))?;
room_updated(&room, &session.peer);
}
@@ -1108,12 +1160,18 @@ async fn join_project(
let collaborators = project
.collaborators
.iter()
.filter(|collaborator| collaborator.connection_id != session.connection_id.0 as i32)
.map(|collaborator| proto::Collaborator {
peer_id: collaborator.connection_id as u32,
replica_id: collaborator.replica_id.0 as u32,
user_id: collaborator.user_id.to_proto(),
.map(|collaborator| {
let peer_id = proto::PeerId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
proto::Collaborator {
peer_id: Some(peer_id),
replica_id: collaborator.replica_id.0 as u32,
user_id: collaborator.user_id.to_proto(),
}
})
.filter(|collaborator| collaborator.peer_id != Some(session.connection_id.into()))
.collect::<Vec<_>>();
let worktrees = project
.worktrees
@@ -1130,11 +1188,11 @@ async fn join_project(
session
.peer
.send(
ConnectionId(collaborator.peer_id),
collaborator.peer_id.unwrap().into(),
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: session.connection_id.0,
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
@@ -1355,13 +1413,14 @@ where
.await
.project_collaborators(project_id, session.connection_id)
.await?;
ConnectionId(
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id as u32,
)
let host = collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?;
ConnectionId {
owner_id: host.connection_server_id.0 as u32,
id: host.connection_id as u32,
}
};
let payload = session
@@ -1389,7 +1448,10 @@ async fn save_buffer(
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?;
ConnectionId(host.connection_id as u32)
ConnectionId {
owner_id: host.connection_server_id.0 as u32,
id: host.connection_id as u32,
}
};
let response_payload = session
.peer
@@ -1401,11 +1463,17 @@ async fn save_buffer(
.await
.project_collaborators(project_id, session.connection_id)
.await?;
collaborators
.retain(|collaborator| collaborator.connection_id != session.connection_id.0 as i32);
let project_connection_ids = collaborators
.iter()
.map(|collaborator| ConnectionId(collaborator.connection_id as u32));
collaborators.retain(|collaborator| {
let collaborator_connection = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
collaborator_connection != session.connection_id
});
let project_connection_ids = collaborators.iter().map(|collaborator| ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
});
broadcast(host_connection_id, project_connection_ids, |conn_id| {
session
.peer
@@ -1419,11 +1487,10 @@ async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
session: Session,
) -> Result<()> {
session.peer.forward_send(
session.connection_id,
ConnectionId(request.peer_id),
request,
)?;
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
session
.peer
.forward_send(session.connection_id, peer_id.into(), request)?;
Ok(())
}
@@ -1516,7 +1583,10 @@ async fn follow(
session: Session,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let leader_id = ConnectionId(request.leader_id);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
{
let project_connection_ids = session
@@ -1536,14 +1606,17 @@ async fn follow(
.await?;
response_payload
.views
.retain(|view| view.leader_id != Some(follower_id.0));
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
Ok(())
}
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let leader_id = ConnectionId(request.leader_id);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let project_connection_ids = session
.db()
.await
@@ -1572,12 +1645,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
});
for follower_id in &request.follower_ids {
let follower_id = ConnectionId(*follower_id);
if project_connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
session
.peer
.forward_send(session.connection_id, follower_id, request.clone())?;
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
if project_connection_ids.contains(&follower_connection_id)
&& Some(follower_peer_id) != leader_id
{
session.peer.forward_send(
session.connection_id,
follower_connection_id,
request.clone(),
)?;
}
}
Ok(())
@@ -1892,13 +1969,19 @@ fn contact_for_user(
fn room_updated(room: &proto::Room, peer: &Peer) {
for participant in &room.participants {
peer.send(
ConnectionId(participant.peer_id),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
if let Some(peer_id) = participant
.peer_id
.ok_or_else(|| anyhow!("invalid participant peer id"))
.trace_err()
{
peer.send(
peer_id.into(),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
}
}
}
@@ -1943,8 +2026,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
let canceled_calls_to_user_ids;
let live_kit_room;
let delete_live_kit_room;
{
let mut left_room = session.db().await.leave_room(session.connection_id).await?;
if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? {
contacts_to_update.insert(session.user_id);
for project in left_room.left_projects.values() {
@@ -1956,6 +2038,8 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut left_room.room.live_kit_room);
delete_live_kit_room = left_room.room.participants.is_empty();
} else {
return Ok(());
}
{
@@ -1982,7 +2066,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit
.remove_participant(live_kit_room.clone(), session.connection_id.to_string())
.remove_participant(live_kit_room.clone(), session.user_id.to_string())
.await
.trace_err();
@@ -2013,7 +2097,7 @@ fn project_left(project: &db::LeftProject, session: &Session) {
*connection_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: session.connection_id.0,
peer_id: Some(session.connection_id.into()),
},
)
.trace_err();

View File

@@ -1,6 +1,6 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
@@ -342,24 +342,27 @@ impl CollabTitlebarItem {
let mut participants = room
.read(cx)
.remote_participants()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.values()
.cloned()
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
.filter_map(|participant| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.get(&participant.peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
Some((
participant.peer_id,
&user.github_login,
participant.location,
)),
workspace,
theme,
cx,
@@ -474,7 +477,7 @@ impl CollabTitlebarItem {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
peer_id.as_u64() as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
@@ -487,22 +490,24 @@ impl CollabTitlebarItem {
.boxed()
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
content
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
.with_tooltip::<JoinProject, _>(
peer_id.0 as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
})
.with_tooltip::<JoinProject, _>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}

View File

@@ -73,7 +73,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace

View File

@@ -2,7 +2,7 @@ use std::{mem, sync::Arc};
use crate::contacts_popover;
use call::ActiveCall;
use client::{Contact, PeerId, User, UserStore};
use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
@@ -461,15 +461,13 @@ impl ContactList {
// Populate remote participants.
self.match_candidates.clear();
self.match_candidates
.extend(
room.remote_participants()
.iter()
.map(|(peer_id, participant)| StringMatchCandidate {
id: peer_id.0 as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}),
);
.extend(room.remote_participants().iter().map(|(_, participant)| {
StringMatchCandidate {
id: participant.user.id as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}
}));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -479,8 +477,8 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
let peer_id = PeerId(mat.candidate_id as u32);
let participant = &room.remote_participants()[&peer_id];
let user_id = mat.candidate_id as u64;
let participant = &room.remote_participants()[&user_id];
participant_entries.push(ContactEntry::CallParticipant {
user: participant.user.clone(),
is_pending: false,
@@ -496,7 +494,7 @@ impl ContactList {
}
if !participant.tracks.is_empty() {
participant_entries.push(ContactEntry::ParticipantScreen {
peer_id,
peer_id: participant.peer_id,
is_last: true,
});
}
@@ -881,75 +879,80 @@ impl ContactList {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
MouseEventHandler::<OpenSharedScreen>::new(peer_id.0 as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
MouseEventHandler::<OpenSharedScreen>::new(
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y =
bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(row.icon.color)
.constrained()
.with_width(row.icon.width)
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
.aligned()
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
})
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(row.icon.color)
.constrained()
.with_width(row.icon.width)
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
.aligned()
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });

View File

@@ -20,8 +20,8 @@ use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{async_iife, ResultExt};
use util::channel::ReleaseChannel;
use util::{async_iife, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA foreign_keys=TRUE;
@@ -39,16 +39,24 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
/// In either case, static variables are set so that the user can be notified.
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
pub async fn open_db<M: Migrator + 'static>(
db_dir: &Path,
release_channel: &ReleaseChannel,
) -> ThreadSafeConnection<M> {
if *ZED_STATELESS {
return open_fallback_db().await;
}
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
@@ -64,11 +72,11 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
//
// Basically: Don't ever push invalid migrations to stable or everyone will have
// a bad time.
// If no db folder, create one at 0-{channel}
create_dir_all(&main_db_dir).context("Could not create db directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
// Optimistically open databases in parallel
if !DB_FILE_OPERATIONS.is_locked() {
// Try building a connection
@@ -76,7 +84,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
return Ok(connection)
};
}
// Take a lock in the failure case so that we move the db once per process instead
// of potentially multiple times from different threads. This shouldn't happen in the
// normal path
@@ -84,12 +92,12 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = open_main_db(&db_path).await {
return Ok(connection)
};
let backup_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
.as_millis();
// If failed, move 0-{channel} to {current unix timestamp}-{channel}
let backup_db_dir = db_dir.join(Path::new(&format!(
"{}-{}",
@@ -105,7 +113,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
let mut guard = BACKUP_DB_PATH.write();
*guard = Some(backup_db_dir);
}
// Create a new 0-{channel}
create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
@@ -117,10 +125,10 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = connection {
return connection;
}
// Set another static ref so that we can escalate the notification
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
// If still failed, create an in memory db with a known name
open_fallback_db().await
}
@@ -174,15 +182,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -205,15 +213,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -232,134 +240,157 @@ macro_rules! define_connection {
mod tests {
use std::{fs, thread};
use sqlez::{domain::Domain, connection::Connection};
use sqlez::{connection::Connection, domain::Domain};
use sqlez_macros::sql;
use tempdir::TempDir;
use crate::{open_db, DB_FILE_NAME};
// Test bad migration panics
#[gpui::test]
#[should_panic]
async fn test_bad_migration_panics() {
enum BadDB {}
impl Domain for BadDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);),
&[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);)]
sql!(CREATE TABLE test(value);),
]
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let mut corrupted_backup_dir = fs::read_dir(
tempdir.path()
).unwrap().find(|entry| {
!entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
}
).unwrap().unwrap().path();
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
let mut corrupted_backup_dir = fs::read_dir(tempdir.path())
.unwrap()
.find(|entry| {
!entry
.as_ref()
.unwrap()
.file_name()
.to_str()
.unwrap()
.starts_with("0")
})
.unwrap()
.unwrap()
.path();
corrupted_backup_dir.push(DB_FILE_NAME);
dbg!(&corrupted_backup_dir);
let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()().unwrap().is_none());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
.unwrap()
.is_none());
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_simultaneous_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
// Setup the bad database
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
// Try to connect to it a bunch of times at once
let mut guards = vec![];
for _ in 0..10 {
let tmp_path = tempdir.path().to_path_buf();
let guard = thread::spawn(move || {
let good_db = smol::block_on(open_db::<GoodDB>(tmp_path.as_path(), &util::channel::ReleaseChannel::Dev));
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let good_db = smol::block_on(open_db::<GoodDB>(
tmp_path.as_path(),
&util::channel::ReleaseChannel::Dev,
));
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
});
guards.push(guard);
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
}
}

View File

@@ -80,7 +80,7 @@ macro_rules! query {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select::<$return_type>(sql_stmt)?(())
self.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
@@ -95,7 +95,7 @@ macro_rules! query {
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select::<$return_type>(sql_stmt)?(())
connection.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),

View File

@@ -164,7 +164,7 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach();
let project = project_handle.read(cx);
@@ -575,6 +575,15 @@ impl Item for ProjectDiagnosticsEditor {
unreachable!()
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
Editor::to_item_events(event)
}

View File

@@ -84,7 +84,7 @@ use std::{
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, Workspace, WorkspaceId};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -467,6 +467,7 @@ pub struct Editor {
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
input_enabled: bool,
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
_subscriptions: Vec<Subscription>,
@@ -1108,6 +1109,7 @@ impl Editor {
keymap_context_layers: Default::default(),
input_enabled: true,
leader_replica_id: None,
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
_subscriptions: vec![
@@ -5451,11 +5453,17 @@ impl Editor {
pub fn set_selections_from_remote(
&mut self,
selections: Vec<Selection<Anchor>>,
pending_selection: Option<Selection<Anchor>>,
cx: &mut ViewContext<Self>,
) {
let old_cursor_position = self.selections.newest_anchor().head();
self.selections.change_with(cx, |s| {
s.select_anchors(selections);
if let Some(pending_selection) = pending_selection {
s.set_pending(pending_selection, SelectMode::Character);
} else {
s.clear_pending();
}
});
self.selections_did_change(false, &old_cursor_position, cx);
}
@@ -5883,25 +5891,36 @@ impl Editor {
fn on_buffer_event(
&mut self,
_: ModelHandle<MultiBuffer>,
event: &language::Event,
event: &multi_buffer::Event,
cx: &mut ViewContext<Self>,
) {
match event {
language::Event::Edited => {
multi_buffer::Event::Edited => {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited);
}
language::Event::Reparsed => cx.emit(Event::Reparsed),
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
language::Event::Saved => cx.emit(Event::Saved),
language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
language::Event::Reloaded => cx.emit(Event::TitleChanged),
language::Event::Closed => cx.emit(Event::Closed),
language::Event::DiagnosticsUpdated => {
multi_buffer::Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => cx.emit(Event::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
excerpts: excerpts.clone(),
}),
multi_buffer::Event::ExcerptsRemoved { ids } => {
cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
multi_buffer::Event::Saved => cx.emit(Event::Saved),
multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
multi_buffer::Event::Closed => cx.emit(Event::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
}
_ => {}
}
}
@@ -6084,8 +6103,16 @@ impl Deref for EditorSnapshot {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
BufferEdited,
Edited,
Reparsed,
@@ -6093,8 +6120,12 @@ pub enum Event {
DirtyChanged,
Saved,
TitleChanged,
SelectionsChanged { local: bool },
ScrollPositionChanged { local: bool },
SelectionsChanged {
local: bool,
},
ScrollPositionChanged {
local: bool,
},
Closed,
}

View File

@@ -1,8 +1,7 @@
use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use std::{cell::RefCell, rc::Rc, time::Instant};
use unindent::Unindent;
use super::*;
@@ -24,7 +23,7 @@ use util::{
};
use workspace::{
item::{FollowableItem, ItemHandle},
NavigationEntry, Pane,
NavigationEntry, Pane, ViewId,
};
#[gpui::test]
@@ -41,7 +40,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor1", *event));
events.borrow_mut().push(("editor1", event.clone()));
}
})
.detach();
@@ -56,7 +55,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor2", *event));
events.borrow_mut().push(("editor2", event.clone()));
}
})
.detach();
@@ -4969,19 +4968,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
}
#[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
async fn test_following(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
cx.set_global(Settings::test(cx));
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
..Default::default()
},
|cx| build_editor(buffer.clone(), cx),
);
let buffer = project.update(cx, |project, cx| {
let buffer = project
.create_buffer(&sample_text(16, 8, 'a'), None, cx)
.unwrap();
cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
});
let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.update(|cx| {
cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
..Default::default()
},
|cx| build_editor(buffer.clone(), cx),
)
});
let is_still_following = Rc::new(RefCell::new(true));
let pending_update = Rc::new(RefCell::new(None));
@@ -5009,44 +5016,50 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
leader.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
assert_eq!(*is_still_following.borrow(), true);
// Update the scroll position only
leader.update(cx, |leader, cx| {
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
assert_eq!(
follower.update(cx, |follower, cx| follower.scroll_position(cx)),
vec2f(1.5, 3.5)
);
assert_eq!(*is_still_following.borrow(), true);
// Update the selections and scroll position
// Update the selections and scroll position. The follower's scroll position is updated
// via autoscroll, not via the leader's exact scroll position.
leader.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
leader.request_autoscroll(Autoscroll::newest(), cx);
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
});
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.update(cx, |follower, cx| {
let initial_scroll_position = follower.scroll_position(cx);
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
assert!(follower.scroll_manager.has_autoscroll_request());
assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
assert_eq!(follower.selections.ranges(cx), vec![0..0]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
assert_eq!(*is_still_following.borrow(), true);
// Creating a pending selection that precedes another selection
@@ -5054,24 +5067,30 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
assert_eq!(*is_still_following.borrow(), true);
// Extend the pending selection so that it surrounds another selection
leader.update(cx, |leader, cx| {
leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![0..2]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
// Scrolling locally breaks the follow
follower.update(cx, |follower, cx| {
@@ -5087,6 +5106,165 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
assert_eq!(*is_still_following.borrow(), false);
}
#[gpui::test]
async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
let leader = pane.update(cx, |_, cx| {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
});
// Start following the editor when it has no excerpts.
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
let follower_1 = cx
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
project.clone(),
ViewId {
creator: Default::default(),
id: 0,
},
&mut state_message,
cx,
)
})
.unwrap()
.await
.unwrap();
let update_message = Rc::new(RefCell::new(None));
follower_1.update(cx, {
let update = update_message.clone();
|_, cx| {
cx.subscribe(&leader, move |_, leader, event, cx| {
leader
.read(cx)
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
})
.detach();
}
});
let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
(
project
.create_buffer("abc\ndef\nghi\njkl\n", None, cx)
.unwrap(),
project
.create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
.unwrap(),
)
});
// Insert some excerpts.
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
let excerpt_ids = multibuffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: 1..6,
primary: None,
},
ExcerptRange {
context: 12..15,
primary: None,
},
ExcerptRange {
context: 0..3,
primary: None,
},
],
cx,
);
multibuffer.insert_excerpts_after(
excerpt_ids[0],
buffer_2.clone(),
[
ExcerptRange {
context: 8..12,
primary: None,
},
ExcerptRange {
context: 0..6,
primary: None,
},
],
cx,
);
});
});
// Apply the update of adding the excerpts.
follower_1
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
assert_eq!(
follower_1.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
update_message.borrow_mut().take();
// Start following separately after it already has excerpts.
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
let follower_2 = cx
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
project.clone(),
ViewId {
creator: Default::default(),
id: 0,
},
&mut state_message,
cx,
)
})
.unwrap()
.await
.unwrap();
assert_eq!(
follower_2.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
// Remove some excerpts.
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
let excerpt_ids = multibuffer.excerpt_ids();
multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
multibuffer.remove_excerpts([excerpt_ids[0]], cx);
});
});
// Apply the update of removing the excerpts.
follower_1
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
follower_2
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
update_message.borrow_mut().take();
assert_eq!(
follower_1.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
}
#[test]
fn test_combine_syntax_and_fuzzy_match_highlights() {
let string = "abcdefghijklmnop";

View File

@@ -1,9 +1,18 @@
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
};
use anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::proto::serialize_anchor as serialize_text_anchor;
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view};
@@ -13,97 +22,140 @@ use std::{
borrow::Cow,
cmp::{self, Ordering},
fmt::Write,
iter,
ops::Range,
path::{Path, PathBuf},
};
use text::Selection;
use util::{ResultExt, TryFutureExt};
use workspace::item::FollowableItemHandle;
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
};
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,
};
pub const MAX_TAB_TITLE_LEN: usize = 24;
impl FollowableItem for Editor {
fn remote_id(&self) -> Option<ViewId> {
self.remote_id
}
fn from_state_proto(
pane: ViewHandle<workspace::Pane>,
project: ModelHandle<Project>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext,
) -> Option<Task<Result<ViewHandle<Self>>>> {
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
if let Some(proto::view::Variant::Editor(state)) = state.take() {
state
} else {
unreachable!()
}
} else {
return None;
};
let Some(proto::view::Variant::Editor(_)) = state else { return None };
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
let buffer = project.update(cx, |project, cx| {
project.open_buffer_by_id(state.buffer_id, cx)
let client = project.read(cx).client();
let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
.iter()
.map(|excerpt| excerpt.buffer_id)
.collect::<HashSet<_>>();
let buffers = project.update(cx, |project, cx| {
buffer_ids
.iter()
.map(|id| project.open_buffer_by_id(*id, cx))
.collect::<Vec<_>>()
});
Some(cx.spawn(|mut cx| async move {
let buffer = buffer.await?;
let editor = pane
.read_with(&cx, |pane, cx| {
pane.items_of_type::<Self>().find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
})
let mut buffers = futures::future::try_join_all(buffers).await?;
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id)
|| state.singleton
&& buffers.len() == 1
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
== Some(&buffers[0])
})
.unwrap_or_else(|| {
pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx))
})
});
});
let editor = editor.unwrap_or_else(|| {
pane.update(&mut cx, |_, cx| {
let multibuffer = cx.add_model(|cx| {
let mut multibuffer;
if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
} else {
multibuffer = MultiBuffer::new(replica_id);
let mut excerpts = state.excerpts.into_iter().peekable();
while let Some(excerpt) = excerpts.peek() {
let buffer_id = excerpt.buffer_id;
let buffer_excerpts = iter::from_fn(|| {
let excerpt = excerpts.peek()?;
(excerpt.buffer_id == buffer_id)
.then(|| excerpts.next().unwrap())
});
let buffer =
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
if let Some(buffer) = buffer {
multibuffer.push_excerpts(
buffer.clone(),
buffer_excerpts.filter_map(deserialize_excerpt_range),
cx,
);
}
}
};
if let Some(title) = &state.title {
multibuffer = multibuffer.with_title(title.clone())
}
multibuffer
});
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
})
});
editor.update(&mut cx, |editor, cx| {
let excerpt_id;
let buffer_id;
{
let buffer = editor.buffer.read(cx).read(cx);
let singleton = buffer.as_singleton().unwrap();
excerpt_id = singleton.0.clone();
buffer_id = singleton.1;
}
editor.remote_id = Some(remote_id);
let buffer = editor.buffer.read(cx).read(cx);
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
if !selections.is_empty() {
editor.set_selections_from_remote(selections, cx);
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(anchor) = state.scroll_top_anchor {
if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: Anchor {
buffer_id: Some(state.buffer_id as usize),
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
top_anchor: scroll_top_anchor,
offset: vec2f(state.scroll_x, state.scroll_y),
},
cx,
);
}
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
})?;
Ok(editor)
}))
}
@@ -134,13 +186,32 @@ impl FollowableItem for Editor {
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
let buffer = self.buffer.read(cx);
let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer
.read(cx)
.excerpts()
.map(|(id, buffer, range)| proto::Excerpt {
id: id.to_proto(),
buffer_id: buffer.remote_id(),
context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range
.primary
.as_ref()
.map(|range| serialize_text_anchor(&range.start)),
primary_end: range
.primary
.as_ref()
.map(|range| serialize_text_anchor(&range.end)),
})
.collect();
Some(proto::view::Variant::Editor(proto::view::Editor {
buffer_id,
scroll_top_anchor: Some(language::proto::serialize_anchor(
&scroll_anchor.top_anchor.text_anchor,
)),
singleton: buffer.is_singleton(),
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
excerpts,
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
scroll_x: scroll_anchor.offset.x(),
scroll_y: scroll_anchor.offset.y(),
selections: self
@@ -149,6 +220,11 @@ impl FollowableItem for Editor {
.iter()
.map(serialize_selection)
.collect(),
pending_selection: self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection),
}))
}
@@ -156,18 +232,43 @@ impl FollowableItem for Editor {
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
_: &AppContext,
cx: &AppContext,
) -> bool {
let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update {
proto::update_view::Variant::Editor(update) => match event {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => {
let buffer_id = buffer.read(cx).remote_id();
let mut excerpts = excerpts.iter();
if let Some((id, range)) = excerpts.next() {
update.inserted_excerpts.push(proto::ExcerptInsertion {
previous_excerpt_id: Some(predecessor.to_proto()),
excerpt: serialize_excerpt(buffer_id, id, range),
});
update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
proto::ExcerptInsertion {
previous_excerpt_id: None,
excerpt: serialize_excerpt(buffer_id, id, range),
}
}))
}
true
}
Event::ExcerptsRemoved { ids } => {
update
.deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto));
true
}
Event::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
&scroll_anchor.top_anchor.text_anchor,
));
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
update.scroll_x = scroll_anchor.offset.x();
update.scroll_y = scroll_anchor.offset.y();
true
@@ -177,9 +278,13 @@ impl FollowableItem for Editor {
.selections
.disjoint_anchors()
.iter()
.chain(self.selections.pending_anchor().as_ref())
.map(serialize_selection)
.collect();
update.pending_selection = self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection);
true
}
_ => false,
@@ -189,45 +294,102 @@ impl FollowableItem for Editor {
fn apply_update_proto(
&mut self,
project: &ModelHandle<Project>,
message: update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()> {
match message {
update_view::Variant::Editor(message) => {
let buffer = self.buffer.read(cx);
let buffer = buffer.read(cx);
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
let excerpt_id = excerpt_id.clone();
drop(buffer);
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let selections = message
.selections
.into_iter()
.filter_map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
})
.collect::<Vec<_>>();
let buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
if !selections.is_empty() {
self.set_selections_from_remote(selections, cx);
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = message.scroll_top_anchor {
self.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
);
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(multibuffer);
let buffers = project.update(cx, |project, cx| {
buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let project = project.clone();
cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?;
this.update(&mut cx, |this, cx| {
this.buffer.update(cx, |multibuffer, cx| {
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {
top_anchor: anchor,
offset: vec2f(message.scroll_x, message.scroll_y)
}, cx);
}
}
}
Ok(())
});
Ok(())
})
}
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
@@ -240,41 +402,82 @@ impl FollowableItem for Editor {
}
}
fn serialize_excerpt(
buffer_id: u64,
id: &ExcerptId,
range: &ExcerptRange<language::Anchor>,
) -> Option<proto::Excerpt> {
Some(proto::Excerpt {
id: id.to_proto(),
buffer_id,
context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range
.primary
.as_ref()
.map(|r| serialize_text_anchor(&r.start)),
primary_end: range
.primary
.as_ref()
.map(|r| serialize_text_anchor(&r.end)),
})
}
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(language::proto::serialize_anchor(
&selection.start.text_anchor,
)),
end: Some(language::proto::serialize_anchor(
&selection.end.text_anchor,
)),
start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)),
reversed: selection.reversed,
}
}
fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
proto::EditorAnchor {
excerpt_id: anchor.excerpt_id.to_proto(),
anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
}
}
fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
let context = {
let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
start..end
};
let primary = excerpt
.primary_start
.zip(excerpt.primary_end)
.and_then(|(start, end)| {
let start = language::proto::deserialize_anchor(start)?;
let end = language::proto::deserialize_anchor(end)?;
Some(start..end)
});
Some(ExcerptRange { context, primary })
}
fn deserialize_selection(
excerpt_id: &ExcerptId,
buffer_id: usize,
buffer: &MultiBufferSnapshot,
selection: proto::Selection,
) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.start?)?,
},
end: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.end?)?,
},
start: deserialize_anchor(buffer, selection.start?)?,
end: deserialize_anchor(buffer, selection.end?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
}
fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
Some(Anchor {
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
})
}
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() {

View File

@@ -9,9 +9,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem,
Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _,
ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
use std::{
@@ -50,6 +50,26 @@ pub struct MultiBuffer {
title: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
Edited,
Reloaded,
Reparsed,
Saved,
FileHandleChanged,
Closed,
DirtyChanged,
DiagnosticsUpdated,
}
#[derive(Clone)]
struct History {
next_transaction_id: TransactionId,
@@ -833,6 +853,30 @@ impl MultiBuffer {
) -> Vec<ExcerptId>
where
O: text::ToOffset,
{
let mut ids = Vec::new();
let mut next_excerpt_id = self.next_excerpt_id;
self.insert_excerpts_with_ids_after(
prev_excerpt_id,
buffer,
ranges.into_iter().map(|range| {
let id = ExcerptId(post_inc(&mut next_excerpt_id));
ids.push(id);
(id, range)
}),
cx,
);
ids
}
pub fn insert_excerpts_with_ids_after<O>(
&mut self,
prev_excerpt_id: ExcerptId,
buffer: ModelHandle<Buffer>,
ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut ModelContext<Self>,
) where
O: text::ToOffset,
{
assert_eq!(self.history.transaction_depth, 0);
let mut ranges = ranges.into_iter().peekable();
@@ -858,7 +902,7 @@ impl MultiBuffer {
cx.observe(&buffer, |_, _, cx| cx.notify()),
cx.subscribe(&buffer, Self::on_buffer_event),
],
buffer,
buffer: buffer.clone(),
});
let mut snapshot = self.snapshot.borrow_mut();
@@ -883,8 +927,8 @@ impl MultiBuffer {
Locator::max()
};
let mut ids = Vec::new();
while let Some(range) = ranges.next() {
let mut excerpts = Vec::new();
while let Some((id, range)) = ranges.next() {
let locator = Locator::between(&prev_locator, &next_locator);
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
buffer_state.excerpts.insert(ix, locator.clone());
@@ -897,7 +941,10 @@ impl MultiBuffer {
..buffer_snapshot.anchor_after(&primary.end)
}),
};
let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
if id.0 >= self.next_excerpt_id {
self.next_excerpt_id = id.0 + 1;
}
excerpts.push((id, range.clone()));
let excerpt = Excerpt::new(
id,
locator.clone(),
@@ -909,7 +956,6 @@ impl MultiBuffer {
new_excerpts.push(excerpt, &());
prev_locator = locator.clone();
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
ids.push(id);
}
let edit_end = new_excerpts.summary().text.len;
@@ -929,12 +975,17 @@ impl MultiBuffer {
new: edit_start..edit_end,
}]);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsAdded {
buffer,
predecessor: prev_excerpt_id,
excerpts,
});
cx.notify();
ids
}
pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
self.sync(cx);
let ids = self.excerpt_ids();
self.buffers.borrow_mut().clear();
let mut snapshot = self.snapshot.borrow_mut();
let prev_len = snapshot.len();
@@ -948,6 +999,7 @@ impl MultiBuffer {
new: 0..0,
}]);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
}
@@ -1071,12 +1123,14 @@ impl MultiBuffer {
cx: &mut ModelContext<Self>,
) {
self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
let mut new_excerpts = SumTree::new();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
let mut edits = Vec::new();
let mut excerpt_ids = excerpt_ids.into_iter().peekable();
let mut excerpt_ids = ids.iter().copied().peekable();
while let Some(excerpt_id) = excerpt_ids.next() {
// Seek to the next excerpt to remove, preserving any preceding excerpts.
@@ -1143,6 +1197,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
}
@@ -1165,10 +1220,22 @@ impl MultiBuffer {
fn on_buffer_event(
&mut self,
_: ModelHandle<Buffer>,
event: &Event,
event: &language::Event,
cx: &mut ModelContext<Self>,
) {
cx.emit(event.clone());
cx.emit(match event {
language::Event::Edited => Event::Edited,
language::Event::DirtyChanged => Event::DirtyChanged,
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
//
language::Event::Operation(_) => return,
});
}
pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
@@ -1604,7 +1671,7 @@ impl MultiBuffer {
}
impl Entity for MultiBuffer {
type Event = language::Event;
type Event = Event;
}
impl MultiBufferSnapshot {
@@ -2450,6 +2517,14 @@ impl MultiBufferSnapshot {
}
}
pub fn excerpts(
&self,
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
self.excerpts
.iter()
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
}
pub fn excerpt_boundaries_in_range<R, T>(
&self,
range: R,
@@ -2635,11 +2710,73 @@ impl MultiBufferSnapshot {
row_range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| {
buffer.git_diff_hunks_in_range(row_range.clone(), reversed)
})
let mut cursor = self.excerpts.cursor::<Point>();
if reversed {
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
if cursor.item().is_none() {
cursor.prev(&());
}
} else {
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
}
std::iter::from_fn(move || {
let excerpt = cursor.item()?;
let multibuffer_start = *cursor.start();
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
if multibuffer_start.row >= row_range.end {
return None;
}
let mut buffer_start = excerpt.range.context.start;
let mut buffer_end = excerpt.range.context.end;
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.start > multibuffer_start.row {
let buffer_start_point =
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
if row_range.end < multibuffer_end.row {
let buffer_end_point =
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
let buffer_hunks = excerpt
.buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
Some(DiffHunk {
buffer_range: start..end,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
})
});
if reversed {
cursor.prev(&());
} else {
cursor.next(&());
}
Some(buffer_hunks)
})
.flatten()
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
@@ -2746,6 +2883,10 @@ impl MultiBufferSnapshot {
}
}
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
let locator = self.excerpt_locator_for_id(excerpt_id);
@@ -3080,6 +3221,14 @@ impl ExcerptId {
Self(usize::MAX)
}
pub fn to_proto(&self) -> u64 {
self.0 as _
}
pub fn from_proto(proto: u64) -> Self {
Self(proto as _)
}
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
let a = snapshot.excerpt_locator_for_id(*self);
let b = snapshot.excerpt_locator_for_id(*other);
@@ -3459,16 +3608,17 @@ impl ToPointUtf16 for PointUtf16 {
#[cfg(test)]
mod tests {
use super::*;
use gpui::MutableAppContext;
use gpui::{MutableAppContext, TestAppContext};
use language::{Buffer, Rope};
use rand::prelude::*;
use settings::Settings;
use std::{env, rc::Rc};
use unindent::Unindent;
use util::test::sample_text;
#[gpui::test]
fn test_singleton_multibuffer(cx: &mut MutableAppContext) {
fn test_singleton(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
@@ -3495,7 +3645,7 @@ mod tests {
}
#[gpui::test]
fn test_remote_multibuffer(cx: &mut MutableAppContext) {
fn test_remote(cx: &mut MutableAppContext) {
let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
let guest_buffer = cx.add_model(|cx| {
let state = host_buffer.read(cx).to_proto();
@@ -3526,7 +3676,7 @@ mod tests {
}
#[gpui::test]
fn test_excerpt_buffer(cx: &mut MutableAppContext) {
fn test_excerpt_boundaries_and_clipping(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -3535,7 +3685,9 @@ mod tests {
multibuffer.update(cx, |_, cx| {
let events = events.clone();
cx.subscribe(&multibuffer, move |_, _, event, _| {
events.borrow_mut().push(event.clone())
if let Event::Edited = event {
events.borrow_mut().push(event.clone())
}
})
.detach();
});
@@ -3748,7 +3900,84 @@ mod tests {
}
#[gpui::test]
fn test_excerpts_with_context_lines(cx: &mut MutableAppContext) {
fn test_excerpt_events(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
follower_multibuffer.update(cx, |_, cx| {
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
match event.clone() {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
_ => {}
}
})
.detach();
});
leader_multibuffer.update(cx, |leader, cx| {
leader.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: 0..8,
primary: None,
},
ExcerptRange {
context: 12..16,
primary: None,
},
],
cx,
);
leader.insert_excerpts_after(
leader.excerpt_ids()[0],
buffer_2.clone(),
[
ExcerptRange {
context: 0..5,
primary: None,
},
ExcerptRange {
context: 10..15,
primary: None,
},
],
cx,
)
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
leader_multibuffer.update(cx, |leader, cx| {
let excerpt_ids = leader.excerpt_ids();
leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
leader_multibuffer.update(cx, |leader, cx| {
leader.clear(cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
}
#[gpui::test]
fn test_push_excerpts_with_context_lines(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
@@ -3784,7 +4013,7 @@ mod tests {
}
#[gpui::test]
fn test_empty_excerpt_buffer(cx: &mut MutableAppContext) {
fn test_empty_multibuffer(cx: &mut MutableAppContext) {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -3872,9 +4101,7 @@ mod tests {
}
#[gpui::test]
fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
cx: &mut MutableAppContext,
) {
fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -4004,6 +4231,178 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
use git::diff::DiffHunkStatus;
// buffer has two modified hunks with two rows each
let buffer_1 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
buffer
});
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
buffer
});
cx.foreground().run_until_parked();
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
buffer_1.clone(),
[
// excerpt ends in the middle of a modified hunk
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt begins in the middle of a modified hunk
ExcerptRange {
context: Point::new(5, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[
// excerpt ends at a deletion
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt starts at a deletion
ExcerptRange {
context: Point::new(2, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains a deletion hunk
ExcerptRange {
context: Point::new(1, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains an insertion hunk
ExcerptRange {
context: Point::new(4, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer
});
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
assert_eq!(
snapshot.text(),
"
1.zero
1.ONE
1.FIVE
1.six
2.zero
2.one
2.two
2.one
2.two
2.four
2.five
2.six"
.unindent()
);
let expected = [
(DiffHunkStatus::Modified, 1..2),
(DiffHunkStatus::Modified, 2..3),
//TODO: Define better when and where removed hunks show up at range extremities
(DiffHunkStatus::Removed, 6..6),
(DiffHunkStatus::Removed, 8..8),
(DiffHunkStatus::Added, 10..11),
];
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, false)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
);
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, true)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.as_slice(),
);
}
#[gpui::test(iterations = 100)]
fn test_random_multibuffer(cx: &mut MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")

View File

@@ -254,7 +254,7 @@ impl<'a> EditorTestContext<'a> {
Actual selections:
{}
"},
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,

View File

@@ -62,11 +62,12 @@ impl View for FileFinder {
impl FileFinder {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path_string = path_match.path.to_string_lossy();
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path_match.path.file_name().map_or_else(
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
@@ -161,7 +162,7 @@ impl FileFinder {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move {
let matches = fuzzy::match_paths(
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
false,

View File

@@ -1,794 +1,8 @@
mod char_bag;
use gpui::executor;
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::atomic::{self, AtomicBool},
sync::Arc,
};
mod matcher;
mod paths;
mod strings;
pub use char_bag::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_strings(
&candidates[segment_start..segment_end],
results,
cancel_flag,
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
pub async fn match_paths<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
matcher.match_paths(
candidate_set.id(),
candidate_set.prefix(),
candidates,
results,
cancel_flag,
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_strings(
&mut self,
candidates: &[StringMatchCandidate],
results: &mut Vec<StringMatch>,
cancel_flag: &AtomicBool,
) {
self.match_internal(
&[],
&[],
candidates.iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
)
}
pub fn match_paths<'c: 'a>(
&mut self,
tree_id: usize,
path_prefix: Arc<str>,
path_entries: impl Iterator<Item = PathMatchCandidate<'c>>,
results: &mut Vec<PathMatch>,
cancel_flag: &AtomicBool,
) {
let prefix = path_prefix.chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
self.match_internal(
&prefix,
&lowercase_prefix,
path_entries,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: tree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: path_prefix.clone(),
},
)
}
fn match_internal<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: path_arcs.get(i).unwrap(),
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_paths(
0,
"".into(),
path_entries.into_iter(),
&mut results,
&cancel_flag,
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}
pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
pub use strings::{match_strings, StringMatch, StringMatchCandidate};

463
crates/fuzzy/src/matcher.rs Normal file
View File

@@ -0,0 +1,463 @@
use std::{
borrow::Cow,
sync::atomic::{self, AtomicBool},
};
use crate::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
pub trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
pub trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_candidates<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use crate::{PathMatch, PathMatchCandidate};
use super::*;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_single_path_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_single_path_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_single_path_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_single_path_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_single_path_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_single_path_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs: Vec<Arc<Path>> = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: &path_arcs[i],
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_candidates(
&[],
&[],
path_entries.into_iter(),
&mut results,
&cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: 0,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: "".into(),
},
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}

174
crates/fuzzy/src/paths.rs Normal file
View File

@@ -0,0 +1,174 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| self.path.cmp(&other.path))
}
}
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
let worktree_id = candidate_set.id();
let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
matcher.match_candidates(
&prefix,
&lowercase_prefix,
candidates,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: candidate_set.prefix(),
},
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

161
crates/fuzzy/src/strings.rs Normal file
View File

@@ -0,0 +1,161 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_candidates(
&[],
&[],
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

View File

@@ -71,18 +71,26 @@ impl BufferDiff {
}
}
pub fn hunks_in_range<'a>(
pub fn hunks_in_row_range<'a>(
&'a self,
query_row_range: Range<u32>,
range: Range<u32>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0));
self.hunks_intersecting_range(start..end, buffer, reversed)
}
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
!before_start && !after_end
});
@@ -141,7 +149,9 @@ impl BufferDiff {
#[cfg(test)]
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.hunks_in_range(0..u32::MAX, text, false)
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
self.hunks_intersecting_range(start..end, text, false)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -355,7 +365,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
diff.hunks_in_range(7..12, &buffer, false),
diff.hunks_in_row_range(7..12, &buffer, false),
&buffer,
&diff_base,
&[

View File

@@ -2310,13 +2310,21 @@ impl BufferSnapshot {
})
}
pub fn git_diff_hunks_in_range<'a>(
pub fn git_diff_hunks_in_row_range<'a>(
&'a self,
query_row_range: Range<u32>,
range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff.hunks_in_row_range(range, self, reversed)
}
pub fn git_diff_hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff
.hunks_in_range(query_row_range, self, reversed)
.hunks_intersecting_range(range, self, reversed)
}
pub fn diagnostics_in_range<'a, T, O>(

View File

@@ -9,7 +9,7 @@ use rpc::proto;
use std::{ops::Range, sync::Arc};
use text::*;
pub use proto::{BufferState, Operation, SelectionSet};
pub use proto::{BufferState, Operation};
pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
match message {
@@ -122,8 +122,14 @@ pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto:
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)),
start: Some(proto::EditorAnchor {
anchor: Some(serialize_anchor(&selection.start)),
excerpt_id: 0,
}),
end: Some(proto::EditorAnchor {
anchor: Some(serialize_anchor(&selection.end)),
excerpt_id: 0,
}),
reversed: selection.reversed,
}
}
@@ -229,8 +235,8 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
.filter_map(|selection| {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?)?,
end: deserialize_anchor(selection.end?)?,
start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
@@ -321,8 +327,8 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?)?,
end: deserialize_anchor(selection.end?)?,
start: deserialize_anchor(selection.start?.anchor?)?,
end: deserialize_anchor(selection.end?.anchor?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})

View File

@@ -84,13 +84,13 @@ impl OutlineView {
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
{
let buffer = editor
let outline = editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
if let Some(outline) = buffer {
if let Some(outline) = outline {
workspace.toggle_modal(cx, |_, cx| {
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
cx.subscribe(&view, Self::on_event).detach();

View File

@@ -3,7 +3,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::{proto, PeerId};
use client::proto::{self, PeerId};
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
point_from_lsp, point_to_lsp,

View File

@@ -7,7 +7,7 @@ pub mod worktree;
mod project_tests;
use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use client::{proto, Client, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{
@@ -15,7 +15,6 @@ use futures::{
future::Shared,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
@@ -103,11 +102,11 @@ pub struct Project {
user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>,
client_state: Option<ProjectClientState>,
collaborators: HashMap<PeerId, Collaborator>,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
_subscriptions: Vec<gpui::Subscription>,
opened_buffer: (watch::Sender<()>, watch::Receiver<()>),
shared_buffers: HashMap<PeerId, HashSet<u64>>,
shared_buffers: HashMap<proto::PeerId, HashSet<u64>>,
#[allow(clippy::type_complexity)]
loading_buffers: HashMap<
ProjectPath,
@@ -164,7 +163,7 @@ enum ProjectClientState {
#[derive(Clone, Debug)]
pub struct Collaborator {
pub peer_id: PeerId,
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
}
@@ -185,7 +184,7 @@ pub enum Event {
},
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
CollaboratorLeft(PeerId),
CollaboratorLeft(proto::PeerId),
}
pub enum LanguageServerState {
@@ -555,7 +554,7 @@ impl Project {
.await?;
let mut collaborators = HashMap::default();
for message in response.collaborators {
let collaborator = Collaborator::from_proto(message);
let collaborator = Collaborator::from_proto(message)?;
collaborators.insert(collaborator.peer_id, collaborator);
}
@@ -754,7 +753,7 @@ impl Project {
}
}
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
&self.collaborators
}
@@ -4605,7 +4604,7 @@ impl Project {
.take()
.ok_or_else(|| anyhow!("empty collaborator"))?;
let collaborator = Collaborator::from_proto(collaborator);
let collaborator = Collaborator::from_proto(collaborator)?;
this.update(&mut cx, |this, cx| {
this.collaborators
.insert(collaborator.peer_id, collaborator);
@@ -4622,7 +4621,10 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let peer_id = PeerId(envelope.payload.peer_id);
let peer_id = envelope
.payload
.peer_id
.ok_or_else(|| anyhow!("invalid peer id"))?;
let replica_id = this
.collaborators
.remove(&peer_id)
@@ -5489,7 +5491,7 @@ impl Project {
fn serialize_project_transaction_for_peer(
&mut self,
project_transaction: ProjectTransaction,
peer_id: PeerId,
peer_id: proto::PeerId,
cx: &AppContext,
) -> proto::ProjectTransaction {
let mut serialized_transaction = proto::ProjectTransaction {
@@ -5545,7 +5547,7 @@ impl Project {
fn create_buffer_for_peer(
&mut self,
buffer: &ModelHandle<Buffer>,
peer_id: PeerId,
peer_id: proto::PeerId,
cx: &AppContext,
) -> u64 {
let buffer_id = buffer.read(cx).remote_id();
@@ -5563,7 +5565,7 @@ impl Project {
client.send(proto::CreateBufferForPeer {
project_id,
peer_id: peer_id.0,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
})?;
@@ -5580,7 +5582,7 @@ impl Project {
let is_last = operations.is_empty();
client.send(proto::CreateBufferForPeer {
project_id,
peer_id: peer_id.0,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::Chunk(
proto::BufferChunk {
buffer_id,
@@ -6036,11 +6038,11 @@ impl Entity for Project {
}
impl Collaborator {
fn from_proto(message: proto::Collaborator) -> Self {
Self {
peer_id: PeerId(message.peer_id),
fn from_proto(message: proto::Collaborator) -> Result<Self> {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
}
})
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "recent_projects"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/recent_projects.rs"
doctest = false
[dependencies]
db = { path = "../db" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }
picker = { path = "../picker" }
settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
postage = { version = "0.4", features = ["futures-traits"] }
smol = "1.2"

View File

@@ -0,0 +1,129 @@
use std::path::Path;
use fuzzy::StringMatch;
use gpui::{
elements::{Label, LabelStyle},
Element, ElementBox,
};
use workspace::WorkspaceLocation;
pub struct HighlightedText {
pub text: String,
pub highlight_positions: Vec<usize>,
char_count: usize,
}
impl HighlightedText {
fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
let mut char_count = 0;
let separator_char_count = separator.chars().count();
let mut text = String::new();
let mut highlight_positions = Vec::new();
for component in components {
if char_count != 0 {
text.push_str(separator);
char_count += separator_char_count;
}
highlight_positions.extend(
component
.highlight_positions
.iter()
.map(|position| position + char_count),
);
text.push_str(&component.text);
char_count += component.text.chars().count();
}
Self {
text,
highlight_positions,
char_count,
}
}
pub fn render(self, style: impl Into<LabelStyle>) -> ElementBox {
Label::new(self.text, style)
.with_highlights(self.highlight_positions)
.boxed()
}
}
pub struct HighlightedWorkspaceLocation {
pub names: HighlightedText,
pub paths: Vec<HighlightedText>,
}
impl HighlightedWorkspaceLocation {
pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
let mut path_start_offset = 0;
let (names, paths): (Vec<_>, Vec<_>) = location
.paths()
.iter()
.map(|path| {
let highlighted_text = Self::highlights_for_path(
path.as_ref(),
&string_match.positions,
path_start_offset,
);
path_start_offset += highlighted_text.1.char_count;
highlighted_text
})
.unzip();
Self {
names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
paths,
}
}
// Compute the highlighted text for the name and path
fn highlights_for_path(
path: &Path,
match_positions: &Vec<usize>,
path_start_offset: usize,
) -> (Option<HighlightedText>, HighlightedText) {
let path_string = path.to_string_lossy();
let path_char_count = path_string.chars().count();
// Get the subset of match highlight positions that line up with the given path.
// Also adjusts them to start at the path start
let path_positions = match_positions
.iter()
.copied()
.skip_while(|position| *position < path_start_offset)
.take_while(|position| *position < path_start_offset + path_char_count)
.map(|position| position - path_start_offset)
.collect::<Vec<_>>();
// Again subset the highlight positions to just those that line up with the file_name
// again adjusted to the start of the file_name
let file_name_text_and_positions = path.file_name().map(|file_name| {
let text = file_name.to_string_lossy();
let char_count = text.chars().count();
let file_name_start = path_char_count - char_count;
let highlight_positions = path_positions
.iter()
.copied()
.skip_while(|position| *position < file_name_start)
.take_while(|position| *position < file_name_start + char_count)
.map(|position| position - file_name_start)
.collect::<Vec<_>>();
HighlightedText {
text: text.to_string(),
highlight_positions,
char_count,
}
});
(
file_name_text_and_positions,
HighlightedText {
text: path_string.to_string(),
highlight_positions: path_positions,
char_count: path_char_count,
},
)
}
}

View File

@@ -0,0 +1,197 @@
mod highlighted_workspace_location;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::{ChildView, Flex, ParentElement},
AnyViewHandle, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::Settings;
use workspace::{OpenPaths, Workspace, WorkspaceLocation, WORKSPACE_DB};
actions!(recent_projects, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(RecentProjectsView::toggle);
Picker::<RecentProjectsView>::init(cx);
}
struct RecentProjectsView {
picker: ViewHandle<Picker<Self>>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
}
impl RecentProjectsView {
fn new(workspace_locations: Vec<WorkspaceLocation>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
}),
workspace_locations,
selected_match_index: 0,
matches: Default::default(),
}
}
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
cx.spawn(|workspace, mut cx| async move {
let workspace_locations = cx
.background()
.spawn(async {
WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect()
})
.await;
workspace.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
cx.subscribe(&view, Self::on_event).detach();
view
});
})
})
.detach();
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
}
}
}
pub enum Event {
Dismissed,
}
impl Entity for RecentProjectsView {
type Event = Event;
}
impl View for RecentProjectsView {
fn ui_name() -> &'static str {
"RecentProjectsView"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for RecentProjectsView {
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_match_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Self>) {
self.selected_match_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
let query = query.trim_start();
let smart_case = query.chars().any(|c| c.is_uppercase());
let candidates = self
.workspace_locations
.iter()
.enumerate()
.map(|(id, location)| {
let combined_string = location
.paths()
.iter()
.map(|path| path.to_string_lossy().to_owned())
.collect::<Vec<_>>()
.join("");
StringMatchCandidate::new(id, combined_string)
})
.collect::<Vec<_>>();
self.matches = smol::block_on(fuzzy::match_strings(
candidates.as_slice(),
query,
smart_case,
100,
&Default::default(),
cx.background().clone(),
));
self.matches.sort_unstable_by_key(|m| m.candidate_id);
self.selected_match_index = self
.matches
.iter()
.enumerate()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
Task::ready(())
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
let selected_match = &self.matches[self.selected_index()];
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
cx.dispatch_global_action(OpenPaths {
paths: workspace_location.paths().as_ref().clone(),
});
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut gpui::MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> ElementBox {
let settings = cx.global::<Settings>();
let string_match = &self.matches[ix];
let style = settings.theme.picker.item.style_for(mouse_state, selected);
let highlighted_location = HighlightedWorkspaceLocation::new(
&string_match,
&self.workspace_locations[string_match.candidate_id],
);
Flex::column()
.with_child(highlighted_location.names.render(style.label.clone()))
.with_children(
highlighted_location
.paths
.into_iter()
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
)
.flex(1., false)
.contained()
.with_style(style.container)
.named("match")
}
}

View File

@@ -1,10 +1,15 @@
syntax = "proto3";
package zed.messages;
message PeerId {
uint32 owner_id = 1;
uint32 id = 2;
}
message Envelope {
uint32 id = 1;
optional uint32 responding_to = 2;
optional uint32 original_sender_id = 3;
optional PeerId original_sender_id = 3;
oneof payload {
Hello hello = 4;
Ack ack = 5;
@@ -125,7 +130,7 @@ message Envelope {
// Messages
message Hello {
uint32 peer_id = 1;
PeerId peer_id = 1;
}
message Ping {}
@@ -167,7 +172,7 @@ message Room {
message Participant {
uint64 user_id = 1;
uint32 peer_id = 2;
PeerId peer_id = 2;
repeated ParticipantProject projects = 3;
ParticipantLocation location = 4;
}
@@ -319,7 +324,7 @@ message AddProjectCollaborator {
message RemoveProjectCollaborator {
uint64 project_id = 1;
uint32 peer_id = 2;
PeerId peer_id = 2;
}
message GetDefinition {
@@ -438,7 +443,7 @@ message OpenBufferResponse {
message CreateBufferForPeer {
uint64 project_id = 1;
uint32 peer_id = 2;
PeerId peer_id = 2;
oneof variant {
BufferState state = 3;
BufferChunk chunk = 4;
@@ -794,17 +799,17 @@ message UpdateDiagnostics {
message Follow {
uint64 project_id = 1;
uint32 leader_id = 2;
PeerId leader_id = 2;
}
message FollowResponse {
optional uint64 active_view_id = 1;
optional ViewId active_view_id = 1;
repeated View views = 2;
}
message UpdateFollowers {
uint64 project_id = 1;
repeated uint32 follower_ids = 2;
repeated PeerId follower_ids = 2;
oneof variant {
UpdateActiveView update_active_view = 3;
View create_view = 4;
@@ -814,7 +819,7 @@ message UpdateFollowers {
message Unfollow {
uint64 project_id = 1;
uint32 leader_id = 2;
PeerId leader_id = 2;
}
message GetPrivateUserInfo {}
@@ -826,46 +831,57 @@ message GetPrivateUserInfoResponse {
// Entities
message ViewId {
PeerId creator = 1;
uint64 id = 2;
}
message UpdateActiveView {
optional uint64 id = 1;
optional uint32 leader_id = 2;
optional ViewId id = 1;
optional PeerId leader_id = 2;
}
message UpdateView {
uint64 id = 1;
optional uint32 leader_id = 2;
ViewId id = 1;
optional PeerId leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
repeated Selection selections = 1;
Anchor scroll_top_anchor = 2;
float scroll_x = 3;
float scroll_y = 4;
repeated ExcerptInsertion inserted_excerpts = 1;
repeated uint64 deleted_excerpts = 2;
repeated Selection selections = 3;
optional Selection pending_selection = 4;
EditorAnchor scroll_top_anchor = 5;
float scroll_x = 6;
float scroll_y = 7;
}
}
message View {
uint64 id = 1;
optional uint32 leader_id = 2;
ViewId id = 1;
optional PeerId leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
uint64 buffer_id = 1;
repeated Selection selections = 2;
Anchor scroll_top_anchor = 3;
float scroll_x = 4;
float scroll_y = 5;
bool singleton = 1;
optional string title = 2;
repeated Excerpt excerpts = 3;
repeated Selection selections = 4;
optional Selection pending_selection = 5;
EditorAnchor scroll_top_anchor = 6;
float scroll_x = 7;
float scroll_y = 8;
}
}
message Collaborator {
uint32 peer_id = 1;
PeerId peer_id = 1;
uint32 replica_id = 2;
uint64 user_id = 3;
}
@@ -913,21 +929,18 @@ enum LineEnding {
Windows = 1;
}
message SelectionSet {
uint32 replica_id = 1;
repeated Selection selections = 2;
uint32 lamport_timestamp = 3;
bool line_mode = 4;
CursorShape cursor_shape = 5;
}
message Selection {
uint64 id = 1;
Anchor start = 2;
Anchor end = 3;
EditorAnchor start = 2;
EditorAnchor end = 3;
bool reversed = 4;
}
message EditorAnchor {
uint64 excerpt_id = 1;
Anchor anchor = 2;
}
enum CursorShape {
CursorBar = 0;
CursorBlock = 1;
@@ -935,6 +948,20 @@ enum CursorShape {
CursorHollow = 3;
}
message ExcerptInsertion {
Excerpt excerpt = 1;
optional uint64 previous_excerpt_id = 2;
}
message Excerpt {
uint64 id = 1;
uint64 buffer_id = 2;
Anchor context_start = 3;
Anchor context_end = 4;
Anchor primary_start = 5;
Anchor primary_end = 6;
}
message Anchor {
uint32 replica_id = 1;
uint32 local_timestamp = 2;

View File

@@ -6,7 +6,10 @@ macro_rules! messages {
$(Some(envelope::Payload::$name(payload)) => {
Some(Box::new(TypedEnvelope {
sender_id,
original_sender_id: envelope.original_sender_id.map(PeerId),
original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId {
owner_id: original_sender.owner_id,
id: original_sender.id
}),
message_id: envelope.id,
payload,
}))
@@ -24,7 +27,7 @@ macro_rules! messages {
self,
id: u32,
responding_to: Option<u32>,
original_sender_id: Option<u32>,
original_sender_id: Option<PeerId>,
) -> Envelope {
Envelope {
id,

View File

@@ -1,5 +1,5 @@
use super::{
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage},
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
Connection,
};
use anyhow::{anyhow, Context, Result};
@@ -11,9 +11,8 @@ use futures::{
};
use parking_lot::{Mutex, RwLock};
use serde::{ser::SerializeStruct, Serialize};
use std::sync::atomic::Ordering::SeqCst;
use std::{fmt, sync::atomic::Ordering::SeqCst};
use std::{
fmt,
future::Future,
marker::PhantomData,
sync::{
@@ -25,20 +24,32 @@ use std::{
use tracing::instrument;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
pub struct ConnectionId(pub u32);
pub struct ConnectionId {
pub owner_id: u32,
pub id: u32,
}
impl fmt::Display for ConnectionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
impl Into<PeerId> for ConnectionId {
fn into(self) -> PeerId {
PeerId {
owner_id: self.owner_id,
id: self.id,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PeerId(pub u32);
impl From<PeerId> for ConnectionId {
fn from(peer_id: PeerId) -> Self {
Self {
owner_id: peer_id.owner_id,
id: peer_id.id,
}
}
}
impl fmt::Display for PeerId {
impl fmt::Display for ConnectionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
write!(f, "{}/{}", self.owner_id, self.id)
}
}
@@ -85,6 +96,7 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
pub struct Peer {
epoch: AtomicU32,
pub connections: RwLock<HashMap<ConnectionId, ConnectionState>>,
next_connection_id: AtomicU32,
}
@@ -105,13 +117,18 @@ const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
impl Peer {
pub fn new() -> Arc<Self> {
pub fn new(epoch: u32) -> Arc<Self> {
Arc::new(Self {
epoch: AtomicU32::new(epoch),
connections: Default::default(),
next_connection_id: Default::default(),
})
}
pub fn epoch(&self) -> u32 {
self.epoch.load(SeqCst)
}
#[instrument(skip_all)]
pub fn add_connection<F, Fut, Out>(
self: &Arc<Self>,
@@ -138,7 +155,10 @@ impl Peer {
let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE);
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst));
let connection_id = ConnectionId {
owner_id: self.epoch.load(SeqCst),
id: self.next_connection_id.fetch_add(1, SeqCst),
};
let connection_state = ConnectionState {
outgoing_tx,
next_message_id: Default::default(),
@@ -255,11 +275,7 @@ impl Peer {
let message_id = incoming.id;
tracing::debug!(?incoming, "incoming message future: start");
let _end = util::defer(move || {
tracing::debug!(
%connection_id,
message_id,
"incoming message future: end"
);
tracing::debug!(%connection_id, message_id, "incoming message future: end");
});
if let Some(responding_to) = incoming.responding_to {
@@ -306,11 +322,7 @@ impl Peer {
None
} else {
tracing::debug!(
%connection_id,
message_id,
"incoming message: received"
);
tracing::debug!(%connection_id, message_id, "incoming message: received");
proto::build_typed_envelope(connection_id, incoming).or_else(|| {
tracing::error!(
%connection_id,
@@ -343,7 +355,13 @@ impl Peer {
self.connections.write().remove(&connection_id);
}
pub fn reset(&self) {
pub fn reset(&self, epoch: u32) {
self.teardown();
self.next_connection_id.store(0, SeqCst);
self.epoch.store(epoch, SeqCst);
}
pub fn teardown(&self) {
self.connections.write().clear();
}
@@ -384,7 +402,7 @@ impl Peer {
.unbounded_send(proto::Message::Envelope(request.into_envelope(
message_id,
None,
original_sender_id.map(|id| id.0),
original_sender_id.map(Into::into),
)))
.map_err(|_| anyhow!("connection was closed"))?;
Ok(())
@@ -433,7 +451,7 @@ impl Peer {
.unbounded_send(proto::Message::Envelope(message.into_envelope(
message_id,
None,
Some(sender_id.0),
Some(sender_id.into()),
)))?;
Ok(())
}
@@ -515,9 +533,9 @@ mod tests {
let executor = cx.foreground();
// create 2 clients connected to 1 server
let server = Peer::new();
let client1 = Peer::new();
let client2 = Peer::new();
let server = Peer::new(0);
let client1 = Peer::new(0);
let client2 = Peer::new(0);
let (client1_to_server_conn, server_to_client_1_conn, _kill) =
Connection::in_memory(cx.background());
@@ -609,8 +627,8 @@ mod tests {
#[gpui::test(iterations = 50)]
async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) {
let executor = cx.foreground();
let server = Peer::new();
let client = Peer::new();
let server = Peer::new(0);
let client = Peer::new(0);
let (client_to_server_conn, server_to_client_conn, _kill) =
Connection::in_memory(cx.background());
@@ -707,8 +725,8 @@ mod tests {
#[gpui::test(iterations = 50)]
async fn test_dropping_request_before_completion(cx: &mut TestAppContext) {
let executor = cx.foreground();
let server = Peer::new();
let client = Peer::new();
let server = Peer::new(0);
let client = Peer::new(0);
let (client_to_server_conn, server_to_client_conn, _kill) =
Connection::in_memory(cx.background());
@@ -822,7 +840,7 @@ mod tests {
let (client_conn, mut server_conn, _kill) = Connection::in_memory(cx.background());
let client = Peer::new();
let client = Peer::new(0);
let (connection_id, io_handler, mut incoming) =
client.add_test_connection(client_conn, cx.background());
@@ -857,7 +875,7 @@ mod tests {
let executor = cx.foreground();
let (client_conn, mut server_conn, _kill) = Connection::in_memory(cx.background());
let client = Peer::new();
let client = Peer::new(0);
let (connection_id, io_handler, mut incoming) =
client.add_test_connection(client_conn, cx.background());
executor.spawn(io_handler).detach();

View File

@@ -1,14 +1,15 @@
use super::{entity_messages, messages, request_messages, ConnectionId, PeerId, TypedEnvelope};
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
use anyhow::{anyhow, Result};
use async_tungstenite::tungstenite::Message as WebSocketMessage;
use futures::{SinkExt as _, StreamExt as _};
use prost::Message as _;
use serde::Serialize;
use std::any::{Any, TypeId};
use std::{cmp, iter, mem};
use std::fmt;
use std::{
cmp,
fmt::Debug,
io,
io, iter, mem,
time::{Duration, SystemTime, UNIX_EPOCH},
};
@@ -21,7 +22,7 @@ pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 's
self,
id: u32,
responding_to: Option<u32>,
original_sender_id: Option<u32>,
original_sender_id: Option<PeerId>,
) -> Envelope;
fn from_envelope(envelope: Envelope) -> Option<Self>;
}
@@ -74,6 +75,49 @@ impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
}
}
impl PeerId {
pub fn from_u64(peer_id: u64) -> Self {
let owner_id = (peer_id >> 32) as u32;
let id = peer_id as u32;
Self { owner_id, id }
}
pub fn as_u64(self) -> u64 {
((self.owner_id as u64) << 32) | (self.id as u64)
}
}
impl Copy for PeerId {}
impl Eq for PeerId {}
impl Ord for PeerId {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.owner_id
.cmp(&other.owner_id)
.then_with(|| self.id.cmp(&other.id))
}
}
impl PartialOrd for PeerId {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::hash::Hash for PeerId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.owner_id.hash(state);
self.id.hash(state);
}
}
impl fmt::Display for PeerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.owner_id, self.id)
}
}
messages!(
(Ack, Foreground),
(AddProjectCollaborator, Foreground),
@@ -477,4 +521,28 @@ mod tests {
stream.read().await.unwrap();
assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
}
#[gpui::test]
fn test_converting_peer_id_from_and_to_u64() {
let peer_id = PeerId {
owner_id: 10,
id: 3,
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
let peer_id = PeerId {
owner_id: u32::MAX,
id: 3,
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
let peer_id = PeerId {
owner_id: 10,
id: u32::MAX,
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
let peer_id = PeerId {
owner_id: u32::MAX,
id: u32::MAX,
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
}
}

View File

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

View File

@@ -334,6 +334,15 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.results_editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
match event {
ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab],
@@ -402,7 +411,7 @@ impl ProjectSearchView {
});
// Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
cx.subscribe(&query_editor, |_, _, event, cx| {
cx.emit(ViewEvent::EditorEvent(*event))
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
@@ -419,7 +428,7 @@ impl ProjectSearchView {
this.update_match_index(cx);
}
// Reraise editor events for workspace item activation purposes
cx.emit(ViewEvent::EditorEvent(*event));
cx.emit(ViewEvent::EditorEvent(event.clone()));
})
.detach();

View File

@@ -597,6 +597,10 @@ where
self.cursor.item()
}
pub fn item_summary(&self) -> Option<&'a T::Summary> {
self.cursor.item_summary()
}
pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
self.cursor.next_internal(&mut self.filter_node, cx);
}

View File

@@ -1496,6 +1496,10 @@ impl BufferSnapshot {
&self.visible_text
}
pub fn remote_id(&self) -> u64 {
self.remote_id
}
pub fn replica_id(&self) -> ReplicaId {
self.replica_id
}

View File

@@ -5,12 +5,15 @@ use std::{
fmt,
path::PathBuf,
rc::Rc,
sync::atomic::{AtomicBool, Ordering},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use anyhow::Result;
use client::proto;
use client::{proto, Client};
use gpui::{
AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
@@ -23,7 +26,8 @@ use util::ResultExt;
use crate::{
pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,
};
#[derive(Eq, PartialEq, Hash)]
@@ -278,9 +282,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers(
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item.id() as u64,
id: followed_item
.remote_id(&workspace.client, cx)
.map(|id| id.to_proto()),
variant: Some(message),
leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
leader_id: workspace.leader_for_pane(&pane),
}),
cx,
);
@@ -332,9 +338,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
this.update_followers(
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item.id() as u64,
id: item
.remote_id(&this.client, cx)
.map(|id| id.to_proto()),
variant: pending_update.borrow_mut().take(),
leader_id: leader_id.map(|id| id.0),
leader_id,
},
),
cx,
@@ -584,10 +592,12 @@ pub trait ProjectItem: Item {
}
pub trait FollowableItem: Item {
fn remote_id(&self) -> Option<ViewId>;
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn from_state_proto(
pane: ViewHandle<Pane>,
project: ModelHandle<Project>,
id: ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext,
) -> Option<Task<Result<ViewHandle<Self>>>>;
@@ -599,15 +609,17 @@ pub trait FollowableItem: Item {
) -> bool;
fn apply_update_proto(
&mut self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()>;
) -> Task<Result<()>>;
fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
}
pub trait FollowableItemHandle: ItemHandle {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto(
@@ -618,13 +630,23 @@ pub trait FollowableItemHandle: ItemHandle {
) -> bool;
fn apply_update_proto(
&self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut MutableAppContext,
) -> Result<()>;
) -> Task<Result<()>>;
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
}
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
self.read(cx).remote_id().or_else(|| {
client.peer_id().map(|creator| ViewId {
creator,
id: self.id() as u64,
})
})
}
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| {
this.set_leader_replica_id(leader_replica_id, cx)
@@ -650,10 +672,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn apply_update_proto(
&self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut MutableAppContext,
) -> Result<()> {
self.update(cx, |this, cx| this.apply_update_proto(message, cx))
) -> Task<Result<()>> {
self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
}
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {

View File

@@ -148,7 +148,7 @@ impl Member {
.and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx);
let collaborator = project.read(cx).collaborators().get(leader_id)?;
let participant = room.remote_participants().get(&leader_id)?;
let participant = room.remote_participant_for_peer_id(*leader_id)?;
Some((collaborator.replica_id, participant))
});

View File

@@ -8,7 +8,7 @@ use anyhow::{anyhow, bail, Context, Result};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::Axis;
use util::{ unzip_option, ResultExt};
use util::{unzip_option, ResultExt};
use crate::dock::DockPosition;
use crate::WorkspaceId;
@@ -31,7 +31,7 @@ define_connection! {
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -43,7 +43,7 @@ define_connection! {
ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -52,7 +52,7 @@ define_connection! {
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
@@ -61,7 +61,7 @@ define_connection! {
ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
@@ -96,10 +96,10 @@ impl WorkspaceDb {
WorkspaceLocation,
bool,
DockPosition,
) =
) =
self.select_row_bound(sql!{
SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
FROM workspaces
FROM workspaces
WHERE workspace_location = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
@@ -119,7 +119,7 @@ impl WorkspaceDb {
.context("Getting center group")
.log_err()?,
dock_position,
left_sidebar_open
left_sidebar_open,
})
}
@@ -158,7 +158,12 @@ impl WorkspaceDb {
dock_visible = ?4,
dock_anchor = ?5,
timestamp = CURRENT_TIMESTAMP
))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
))?((
workspace.id,
&workspace.location,
workspace.left_sidebar_open,
workspace.dock_position,
))
.context("Updating workspace")?;
// Save center pane group and dock pane
@@ -190,15 +195,38 @@ impl WorkspaceDb {
}
query! {
pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
FROM workspaces
WHERE workspace_location IS NOT NULL
ORDER BY timestamp DESC
LIMIT ?
ORDER BY timestamp DESC
}
}
query! {
async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
DELETE FROM workspaces
WHERE workspace_id IS ?
}
}
// Returns the recent locations which are still valid on disk and deletes ones which no longer
// exist.
pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
let mut result = Vec::new();
let mut delete_tasks = Vec::new();
for (id, location) in self.recent_workspaces()? {
if location.paths().iter().all(|path| dbg!(path).exists()) {
result.push((id, location));
} else {
delete_tasks.push(self.delete_stale_workspace(id));
}
}
futures::future::join_all(delete_tasks).await;
Ok(result)
}
query! {
pub fn last_workspace() -> Result<Option<WorkspaceLocation>> {
SELECT workspace_location
@@ -210,10 +238,16 @@ impl WorkspaceDb {
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
Ok(self.get_pane_group(workspace_id, None)?
Ok(self
.get_pane_group(workspace_id, None)?
.into_iter()
.next()
.unwrap_or_else(|| SerializedPaneGroup::Pane(SerializedPane { active: true, children: vec![] })))
.unwrap_or_else(|| {
SerializedPaneGroup::Pane(SerializedPane {
active: true,
children: vec![],
})
}))
}
fn get_pane_group(
@@ -225,7 +259,7 @@ impl WorkspaceDb {
type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
self.select_bound::<GroupKey, GroupOrPane>(sql!(
SELECT group_id, axis, pane_id, active
FROM (SELECT
FROM (SELECT
group_id,
axis,
NULL as pane_id,
@@ -233,18 +267,18 @@ impl WorkspaceDb {
position,
parent_group_id,
workspace_id
FROM pane_groups
FROM pane_groups
UNION
SELECT
SELECT
NULL,
NULL,
NULL,
center_panes.pane_id,
panes.active as active,
position,
parent_group_id,
panes.workspace_id as workspace_id
FROM center_panes
JOIN panes ON center_panes.pane_id = panes.pane_id)
JOIN panes ON center_panes.pane_id = panes.pane_id)
WHERE parent_group_id IS ? AND workspace_id = ?
ORDER BY position
))?((group_id, workspace_id))?
@@ -267,13 +301,12 @@ impl WorkspaceDb {
// Filter out panes and pane groups which don't have any children or items
.filter(|pane_group| match pane_group {
Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
_ => true,
})
.collect::<Result<_>>()
}
fn save_pane_group(
conn: &Connection,
workspace_id: WorkspaceId,
@@ -285,15 +318,10 @@ impl WorkspaceDb {
let (parent_id, position) = unzip_option(parent);
let group_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
VALUES (?, ?, ?, ?)
INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
VALUES (?, ?, ?, ?)
RETURNING group_id
))?((
workspace_id,
parent_id,
position,
*axis,
))?
))?((workspace_id, parent_id, position, *axis))?
.ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
for (position, group) in children.iter().enumerate() {
@@ -314,9 +342,7 @@ impl WorkspaceDb {
SELECT pane_id, active
FROM panes
WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
))?(
workspace_id,
)?
))?(workspace_id)?
.context("No dock pane for workspace")?;
Ok(SerializedPane::new(
@@ -333,8 +359,8 @@ impl WorkspaceDb {
dock: bool,
) -> Result<PaneId> {
let pane_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
RETURNING pane_id
))?((workspace_id, pane.active))?
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
@@ -376,14 +402,13 @@ impl WorkspaceDb {
Ok(())
}
query!{
query! {
pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
UPDATE workspaces
SET timestamp = CURRENT_TIMESTAMP
WHERE workspace_id = ?
}
}
}
#[cfg(test)]
@@ -472,7 +497,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: true
left_sidebar_open: true,
};
let mut workspace_2 = SerializedWorkspace {
@@ -481,7 +506,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: false
left_sidebar_open: false,
};
db.save_workspace(workspace_1.clone()).await;
@@ -587,7 +612,7 @@ mod tests {
dock_position: DockPosition::Shown(DockAnchor::Bottom),
center_group,
dock_pane,
left_sidebar_open: true
left_sidebar_open: true,
};
db.save_workspace(workspace.clone()).await;
@@ -660,7 +685,7 @@ mod tests {
dock_position: DockPosition::Shown(DockAnchor::Right),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: false
left_sidebar_open: false,
};
db.save_workspace(workspace_3.clone()).await;
@@ -695,7 +720,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
center_group: center_group.clone(),
dock_pane,
left_sidebar_open: true
left_sidebar_open: true,
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use call::participant::{Frame, RemoteVideoTrack};
use client::{PeerId, User};
use client::{proto::PeerId, User};
use futures::StreamExt;
use gpui::{
elements::*,

View File

@@ -14,23 +14,21 @@ pub mod sidebar;
mod status_bar;
mod toolbar;
use std::{
any::TypeId,
borrow::Cow,
future::Future,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Result};
use call::ActiveCall;
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use fs::{self, Fs};
use futures::{channel::oneshot, FutureExt, StreamExt};
use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
FutureExt, StreamExt,
};
use gpui::{
actions,
elements::*,
@@ -42,7 +40,19 @@ use gpui::{
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use language::LanguageRegistry;
use std::{
any::TypeId,
borrow::Cow,
future::Future,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use crate::{
notifications::simple_message_notification::{MessageNotification, OsOpen},
persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
};
use log::{error, warn};
use notifications::NotificationHandle;
pub use pane::*;
@@ -50,7 +60,7 @@ pub use pane_group::*;
use persistence::{model::SerializedItem, DB};
pub use persistence::{
model::{ItemId, WorkspaceLocation},
WorkspaceDb,
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@@ -64,11 +74,6 @@ use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt;
use crate::{
notifications::simple_message_notification::{MessageNotification, OsOpen},
persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
};
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
@@ -316,6 +321,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
type FollowableItemBuilder = fn(
ViewHandle<Pane>,
ModelHandle<Project>,
ViewId,
&mut Option<proto::view::Variant>,
&mut MutableAppContext,
) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
@@ -331,8 +337,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
builders.insert(
TypeId::of::<I>(),
(
|pane, project, state, cx| {
I::from_state_proto(pane, project, state, cx).map(|task| {
|pane, project, id, state, cx| {
I::from_state_proto(pane, project, id, state, cx).map(|task| {
cx.foreground()
.spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
})
@@ -458,25 +464,6 @@ impl DelayedDebouncedEditAction {
}
}
#[derive(Default)]
struct LeaderState {
followers: HashSet<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
active_view_id: Option<u64>,
items_by_leader_view_id: HashMap<u64, FollowerItem>,
}
#[derive(Debug)]
enum FollowerItem {
Loading(Vec<proto::update_view::Variant>),
Loaded(Box<dyn FollowableItemHandle>),
}
pub enum Event {
DockAnchorChanged,
PaneAdded(ViewHandle<Pane>),
@@ -507,10 +494,31 @@ pub struct Workspace {
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<()>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: PeerId,
pub id: u64,
}
#[derive(Default)]
struct LeaderState {
followers: HashSet<PeerId>,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
impl Workspace {
pub fn new(
serialized_workspace: Option<SerializedWorkspace>,
@@ -576,10 +584,24 @@ impl Workspace {
})
}
});
let handle = cx.handle();
let weak_handle = cx.weak_handle();
// All leader updates are enqueued and then processed in a single task, so
// that each asynchronous operation can be run in order.
let (leader_updates_tx, mut leader_updates_rx) =
mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
let _apply_leader_updates = cx.spawn_weak(|this, mut cx| async move {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
let Some(this) = this.upgrade(&cx) else { break };
Self::process_leader_update(this, leader_id, update, &mut cx)
.await
.log_err();
}
Ok(())
});
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
@@ -637,6 +659,8 @@ impl Workspace {
active_call,
database_id: workspace_id,
_observe_current_user,
_apply_leader_updates,
leader_updates_tx,
};
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx));
@@ -1440,8 +1464,12 @@ impl Workspace {
self.update_followers(
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
id: self.active_item(cx).map(|item| item.id() as u64),
leader_id: self.leader_for_pane(&pane).map(|id| id.0),
id: self.active_item(cx).and_then(|item| {
item.to_followable_item_handle(cx)?
.remote_id(&self.client, cx)
.map(|id| id.to_proto())
}),
leader_id: self.leader_for_pane(&pane),
}),
cx,
);
@@ -1586,9 +1614,7 @@ impl Workspace {
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() {
if let FollowerItem::Loaded(item) = item {
item.set_leader_replica_id(None, cx);
}
item.set_leader_replica_id(None, cx);
}
}
}
@@ -1620,7 +1646,7 @@ impl Workspace {
let project_id = self.project.read(cx).remote_id()?;
let request = self.client.request(proto::Follow {
project_id,
leader_id: leader_id.0,
leader_id: Some(leader_id),
});
Some(cx.spawn_weak(|this, mut cx| async move {
let response = request.await?;
@@ -1631,11 +1657,22 @@ impl Workspace {
.get_mut(&leader_id)
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
.ok_or_else(|| anyhow!("following interrupted"))?;
state.active_view_id = response.active_view_id;
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
Some(ViewId::from_proto(active_view_id)?)
} else {
None
};
Ok::<_, anyhow::Error>(())
})?;
Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
.await?;
Self::add_views_from_leader(
this.clone(),
leader_id,
vec![pane],
response.views,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
}
Ok(())
}))
@@ -1681,9 +1718,7 @@ impl Workspace {
let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id {
if let FollowerItem::Loaded(item) = item {
item.set_leader_replica_id(None, cx);
}
item.set_leader_replica_id(None, cx);
}
if states_by_pane.is_empty() {
@@ -1692,7 +1727,7 @@ impl Workspace {
self.client
.send(proto::Unfollow {
project_id,
leader_id: leader_id.0,
leader_id: Some(leader_id),
})
.log_err();
}
@@ -1874,29 +1909,33 @@ impl Workspace {
mut cx: AsyncAppContext,
) -> Result<proto::FollowResponse> {
this.update(&mut cx, |this, cx| {
let client = &this.client;
this.leader_state
.followers
.insert(envelope.original_sender_id()?);
let active_view_id = this
.active_item(cx)
.and_then(|i| i.to_followable_item_handle(cx))
.map(|i| i.id() as u64);
let active_view_id = this.active_item(cx).and_then(|i| {
Some(
i.to_followable_item_handle(cx)?
.remote_id(client, cx)?
.to_proto(),
)
});
Ok(proto::FollowResponse {
active_view_id,
views: this
.panes()
.iter()
.flat_map(|pane| {
let leader_id = this.leader_for_pane(pane).map(|id| id.0);
let leader_id = this.leader_for_pane(pane);
pane.read(cx).items().filter_map({
let cx = &cx;
move |item| {
let id = item.id() as u64;
let item = item.to_followable_item_handle(cx)?;
let id = item.remote_id(client, cx)?.to_proto();
let variant = item.to_state_proto(cx)?;
Some(proto::View {
id,
id: Some(id),
leader_id,
variant: Some(variant),
})
@@ -1926,45 +1965,62 @@ impl Workspace {
this: ViewHandle<Self>,
envelope: TypedEnvelope<proto::UpdateFollowers>,
_: Arc<Client>,
mut cx: AsyncAppContext,
cx: AsyncAppContext,
) -> Result<()> {
let leader_id = envelope.original_sender_id()?;
match envelope
.payload
.variant
.ok_or_else(|| anyhow!("invalid update"))?
{
this.read_with(&cx, |this, _| {
this.leader_updates_tx
.unbounded_send((leader_id, envelope.payload))
})?;
Ok(())
}
async fn process_leader_update(
this: ViewHandle<Self>,
leader_id: PeerId,
update: proto::UpdateFollowers,
cx: &mut AsyncAppContext,
) -> Result<()> {
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(&mut cx, |this, cx| {
this.update_leader_state(leader_id, cx, |state, _| {
state.active_view_id = update_active_view.id;
});
Ok::<_, anyhow::Error>(())
})
this.update(cx, |this, _| {
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
for state in state.values_mut() {
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
} else {
None
};
}
}
anyhow::Ok(())
})?;
}
proto::update_followers::Variant::UpdateView(update_view) => {
this.update(&mut cx, |this, cx| {
let variant = update_view
.variant
.ok_or_else(|| anyhow!("missing update view variant"))?;
this.update_leader_state(leader_id, cx, |state, cx| {
let variant = variant.clone();
match state
.items_by_leader_view_id
.entry(update_view.id)
.or_insert(FollowerItem::Loading(Vec::new()))
{
FollowerItem::Loaded(item) => {
item.apply_update_proto(variant, cx).log_err();
let variant = update_view
.variant
.ok_or_else(|| anyhow!("missing update view variant"))?;
let id = update_view
.id
.ok_or_else(|| anyhow!("missing update view id"))?;
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() {
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));
}
FollowerItem::Loading(updates) => updates.push(variant),
}
});
Ok(())
})
}
anyhow::Ok(())
})?;
try_join_all(tasks).await.log_err();
}
proto::update_followers::Variant::CreateView(view) => {
let panes = this.read_with(&cx, |this, _| {
let panes = this.read_with(cx, |this, _| {
this.follower_states_by_leader
.get(&leader_id)
.into_iter()
@@ -1972,13 +2028,10 @@ impl Workspace {
.cloned()
.collect()
});
Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
.await?;
Ok(())
Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
}
}
.log_err();
this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
Ok(())
}
@@ -2011,16 +2064,19 @@ impl Workspace {
let mut item_tasks = Vec::new();
let mut leader_view_ids = Vec::new();
for view in &views {
let Some(id) = &view.id else { continue };
let id = ViewId::from_proto(id.clone())?;
let mut variant = view.variant.clone();
if variant.is_none() {
Err(anyhow!("missing variant"))?;
}
for build_item in &item_builders {
let task =
cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
let task = cx.update(|cx| {
build_item(pane.clone(), project.clone(), id, &mut variant, cx)
});
if let Some(task) = task {
item_tasks.push(task);
leader_view_ids.push(view.id);
leader_view_ids.push(id);
break;
} else {
assert!(variant.is_some());
@@ -2041,29 +2097,12 @@ impl Workspace {
for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_replica_id(Some(replica_id), cx);
match state.items_by_leader_view_id.entry(id) {
hash_map::Entry::Occupied(e) => {
let e = e.into_mut();
if let FollowerItem::Loading(updates) = e {
for update in updates.drain(..) {
item.apply_update_proto(update, cx)
.context("failed to apply view update")
.log_err();
}
}
*e = FollowerItem::Loaded(item);
}
hash_map::Entry::Vacant(e) => {
e.insert(FollowerItem::Loaded(item));
}
}
state.items_by_leader_view_id.insert(id, item);
}
Some(())
});
}
this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
Ok(())
}
@@ -2077,7 +2116,7 @@ impl Workspace {
self.client
.send(proto::UpdateFollowers {
project_id,
follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
follower_ids: self.leader_state.followers.iter().copied().collect(),
variant: Some(update),
})
.log_err();
@@ -2097,36 +2136,19 @@ impl Workspace {
})
}
fn update_leader_state(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
) {
for (_, state) in self
.follower_states_by_leader
.get_mut(&leader_id)
.into_iter()
.flatten()
{
update_fn(state, cx);
}
self.leader_updated(leader_id, cx);
}
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
cx.notify();
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participants().get(&leader_id)?;
let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_add = Vec::new();
match participant.location {
call::ParticipantLocation::SharedProject { project_id } => {
if Some(project_id) == self.project.read(cx).remote_id() {
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
if let Some(FollowerItem::Loaded(item)) = state
if let Some(item) = state
.active_view_id
.and_then(|id| state.items_by_leader_view_id.get(&id))
{
@@ -2168,7 +2190,7 @@ impl Workspace {
) -> Option<ViewHandle<SharedScreen>> {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participants().get(&peer_id)?;
let participant = room.remote_participant_for_peer_id(peer_id)?;
let track = participant.tracks.values().next()?.clone();
let user = participant.user.clone();
@@ -2575,6 +2597,24 @@ impl View for Workspace {
}
}
impl ViewId {
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
Ok(Self {
creator: message
.creator
.ok_or_else(|| anyhow!("creator is missing"))?,
id: message.id,
})
}
pub(crate) fn to_proto(&self) -> proto::ViewId {
proto::ViewId {
creator: Some(self.creator),
id: self.id,
}
}
}
pub trait WorkspaceHandle {
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
}

View File

@@ -44,6 +44,7 @@ plugin_runtime = { path = "../plugin_runtime" }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -93,7 +93,7 @@ impl LspAdapter for RustLspAdapter {
}
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
Some("rust-analyzer/checkOnSave".into())
Some("rust-analyzer/flycheck".into())
}
async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {

View File

@@ -123,6 +123,7 @@ fn main() {
vim::init(cx);
terminal_view::init(cx);
theme_testbench::init(cx);
recent_projects::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();

View File

@@ -79,6 +79,11 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Open…",
action: Box::new(workspace::Open),
},
MenuItem::Action {
name: "Open Recent...",
action: Box::new(recent_projects::Toggle),
},
MenuItem::Separator,
MenuItem::Action {
name: "Add Folder to Project…",
action: Box::new(workspace::AddFolderToProject),

View File

@@ -15,12 +15,16 @@ use editor::{Editor, MultiBuffer};
use gpui::{
actions,
geometry::vector::vec2f,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions,
platform::{WindowBounds, WindowOptions},
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
};
use language::Rope;
use lazy_static::lazy_static;
pub use lsp;
pub use project;
use project_panel::ProjectPanel;
@@ -68,6 +72,17 @@ actions!(
const MIN_FONT_SIZE: f32 = 6.0;
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
}
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
@@ -336,8 +351,13 @@ pub fn initialize_workspace(
}
pub fn build_window_options() -> WindowOptions<'static> {
let bounds = if let Some((position, size)) = ZED_WINDOW_POSITION.zip(*ZED_WINDOW_SIZE) {
WindowBounds::Fixed(RectF::new(position, size))
} else {
WindowBounds::Maximized
};
WindowOptions {
bounds: WindowBounds::Maximized,
bounds,
titlebar: Some(TitlebarOptions {
title: None,
appears_transparent: true,
@@ -612,6 +632,13 @@ fn schema_file_match(path: &Path) -> &Path {
.unwrap()
}
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
let mut parts = value.split(',');
let width: usize = parts.next()?.parse().ok()?;
let height: usize = parts.next()?.parse().ok()?;
Some(vec2f(width as f32, height as f32))
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -51,13 +51,13 @@ cp -R target/x86_64-apple-darwin/release/WebRTC.framework "${app_path}/Contents/
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Signing bundle with Apple-issued certificate"
security create-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain || echo ""
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
security default-keychain -s zed.keychain
security unlock-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain
echo $MACOS_CERTIFICATE | base64 --decode > /tmp/zed-certificate.p12
security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
rm /tmp/zed-certificate.p12
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}" -v
security default-keychain -s login.keychain
else
@@ -66,22 +66,31 @@ else
codesign --force --deep --sign - "${app_path}" -v
fi
dmg_target_directory="target/release"
dmg_source_directory="${dmg_target_directory}/dmg"
dmg_file_path="${dmg_target_directory}/Zed.dmg"
echo "Creating DMG"
mkdir -p target/release/dmg
rm -rf target/release/dmg/*
mv "${app_path}" target/release/dmg/
hdiutil create -volname Zed -srcfolder target/release/dmg -ov -format UDZO target/release/Zed.dmg
rm -rf ${dmg_source_directory}
mkdir -p ${dmg_source_directory}
mv "${app_path}" "${dmg_source_directory}"
ln -s /Applications ${dmg_source_directory}
hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`.
# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now.
rm ${dmg_source_directory}/Applications
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Notarizing DMG with Apple"
npm install -g notarize-cli
npx notarize-cli --file target/release/Zed.dmg --bundle-id dev.zed.Zed --username $APPLE_NOTARIZATION_USERNAME --password $APPLE_NOTARIZATION_PASSWORD
npx notarize-cli --file ${dmg_file_path} --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
fi
# If -o option is specified, open the target/release directory in Finder to reveal the DMG
# If -o option is specified, open the $dmg_target_directory directory in Finder to reveal the DMG
while getopts o flag
do
case "${flag}" in
o) open target/release;;
o) open $dmg_target_directory;;
esac
done

View File

@@ -0,0 +1,51 @@
#!/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
# 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.
resolution_line=$(system_profiler SPDisplaysDataType | grep Resolution | head -n1)
screen_size=($(echo $resolution_line | egrep -o '[0-9]+'))
scale_factor=1
if [[ $resolution_line =~ Retina ]]; then scale_factor=2; fi
width=$(expr ${screen_size[0]} / 2 / $scale_factor)
height=${screen_size[1] / $scale_factor}
position_1=0,0
position_2=${width},0
# Authenticate using the collab server's admin secret.
export ZED_STATELESS=1
export ZED_ADMIN_API_TOKEN=secret
export ZED_SERVER_URL=http://localhost:8080
export ZED_WINDOW_SIZE=${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=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
wait

View File

@@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets {
"negative",
].includes(key);
}
function isStyle(key: any): key is Styles {
return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key);
return [
"default",
"active",
"disabled",
"hovered",
"pressed",
"inverted",
].includes(key);
}
function getStyle(
layer: Layer,