Compare commits
83 Commits
collab-v0.
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8b209306e | ||
|
|
61c6c825b5 | ||
|
|
6f211292b2 | ||
|
|
c49573dc11 | ||
|
|
de9c58d216 | ||
|
|
84a860e54d | ||
|
|
cb60eb8a57 | ||
|
|
1e02ebbd11 | ||
|
|
8c64514570 | ||
|
|
6fcb3c9020 | ||
|
|
2c47bd4a97 | ||
|
|
a5f624203e | ||
|
|
98d1b6ec5a | ||
|
|
457e1046c8 | ||
|
|
21ab1bb434 | ||
|
|
aa44de3d16 | ||
|
|
ad37034960 | ||
|
|
ebd0c5d000 | ||
|
|
f88b413f6a | ||
|
|
c2f5381e5a | ||
|
|
ea1f6689b9 | ||
|
|
b1affb13bb | ||
|
|
2679e245a5 | ||
|
|
5a334622ea | ||
|
|
5720c43fe7 | ||
|
|
af4d846428 | ||
|
|
5fb522a9b1 | ||
|
|
86e5ae1f2e | ||
|
|
aadd7f2886 | ||
|
|
067a19c971 | ||
|
|
688f179256 | ||
|
|
af77f1188a | ||
|
|
0dedc1f3a4 | ||
|
|
6c58a4f885 | ||
|
|
81e3b48f37 | ||
|
|
6da59311d1 | ||
|
|
2bc685281c | ||
|
|
7e0b6ed1c6 | ||
|
|
e08d6cd6de | ||
|
|
954c9ac3fd | ||
|
|
e4c5dfcf6c | ||
|
|
5f6313d336 | ||
|
|
70efd2bebe | ||
|
|
43b7e16c89 | ||
|
|
f99f581bfc | ||
|
|
09d3fbf04f | ||
|
|
363e3cae4b | ||
|
|
930be6706f | ||
|
|
05e99eb67e | ||
|
|
9bd400cf16 | ||
|
|
553585b9a1 | ||
|
|
674fddac87 | ||
|
|
63e7b9189d | ||
|
|
9530976f61 | ||
|
|
02c30b0091 | ||
|
|
b9c7796547 | ||
|
|
e00cb6b074 | ||
|
|
dc47552180 | ||
|
|
98a593b263 | ||
|
|
897506c797 | ||
|
|
59c9a57570 | ||
|
|
dde6cf596e | ||
|
|
2596fefa04 | ||
|
|
34b69896e4 | ||
|
|
7824ace58b | ||
|
|
b150efbd96 | ||
|
|
cf72173282 | ||
|
|
ecd44e6914 | ||
|
|
2cd9987b54 | ||
|
|
7c3dc1e3dc | ||
|
|
00b7c78e33 | ||
|
|
11800a8a78 | ||
|
|
f797dfb88f | ||
|
|
7608875625 | ||
|
|
dcf11ac7e5 | ||
|
|
82824f78b6 | ||
|
|
e4507c1d74 | ||
|
|
9314c0e313 | ||
|
|
a48cd9125b | ||
|
|
6120d6488b | ||
|
|
82abf31ef1 | ||
|
|
6d9b55a654 | ||
|
|
3eac3e20d5 |
22
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -40,6 +40,7 @@ members = [
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/recent_projects",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/search",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
15
crates/collab/src/db/server.rs
Normal 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 {}
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -254,7 +254,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
|
||||
Actual selections:
|
||||
{}
|
||||
"},
|
||||
"},
|
||||
self.assertion_context(),
|
||||
expected_marked_text,
|
||||
actual_marked_text,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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γdδ", "αβγδ/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γdδ", 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
@@ -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γdδ", "αβγδ/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γdδ", 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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
&[
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
crates/recent_projects/Cargo.toml
Normal 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"
|
||||
129
crates/recent_projects/src/highlighted_workspace_location.rs
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
197
crates/recent_projects/src/recent_projects.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::*,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 275 KiB |
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
51
script/start-local-collaboration
Executable 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
|
||||
@@ -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,
|
||||
|
||||