Detailed changes
@@ -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.0"
+version = "0.4.0"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -4463,6 +4464,7 @@ dependencies = [
"smol",
"sum_tree",
"tempdir",
+ "terminal",
"text",
"thiserror",
"toml",
@@ -6259,6 +6261,32 @@ name = "terminal"
version = "0.1.0"
dependencies = [
"alacritty_terminal",
+ "anyhow",
+ "db",
+ "dirs 4.0.0",
+ "futures 0.3.25",
+ "gpui",
+ "itertools",
+ "lazy_static",
+ "libc",
+ "mio-extras",
+ "ordered-float",
+ "procinfo",
+ "rand 0.8.5",
+ "serde",
+ "settings",
+ "shellexpand",
+ "smallvec",
+ "smol",
+ "theme",
+ "thiserror",
+ "util",
+]
+
+[[package]]
+name = "terminal_view"
+version = "0.1.0"
+dependencies = [
"anyhow",
"client",
"context_menu",
@@ -6281,6 +6309,7 @@ dependencies = [
"shellexpand",
"smallvec",
"smol",
+ "terminal",
"theme",
"thiserror",
"util",
@@ -8166,7 +8195,7 @@ dependencies = [
"smol",
"sum_tree",
"tempdir",
- "terminal",
+ "terminal_view",
"text",
"theme",
"theme_selector",
@@ -28,7 +28,7 @@ impl View for UpdateNotification {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = cx.global::<Settings>().theme.clone();
- let theme = &theme.simple_message_notification;
+ let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().display_name();
@@ -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" }
@@ -3,7 +3,7 @@ use crate::{
IncomingCall,
};
use anyhow::{anyhow, Result};
-use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
+use client::{proto, Client, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use futures::{FutureExt, StreamExt};
use gpui::{
@@ -13,17 +13,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 +41,7 @@ pub struct Room {
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
local_participant: LocalParticipant,
- remote_participants: BTreeMap<PeerId, RemoteParticipant>,
+ remote_participants: BTreeMap<proto::PeerId, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
@@ -50,7 +50,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 +58,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 +123,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 +230,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 +256,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 +272,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 +293,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 +311,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 +327,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,7 +349,7 @@ impl Room {
&self.local_participant
}
- pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
+ pub fn remote_participants(&self) -> &BTreeMap<proto::PeerId, RemoteParticipant> {
&self.remote_participants
}
@@ -407,7 +419,7 @@ 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);
@@ -464,7 +476,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),
@@ -499,6 +511,7 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
+ log::info!("room is empty, leaving");
let _ = this.leave(cx);
}
@@ -518,7 +531,7 @@ impl Room {
) -> Result<()> {
match change {
RemoteVideoTrackUpdate::Subscribed(track) => {
- let peer_id = PeerId(track.publisher_id().parse()?);
+ let peer_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
@@ -538,7 +551,7 @@ impl Room {
publisher_id,
track_id,
} => {
- let peer_id = PeerId(publisher_id.parse()?);
+ let peer_id = publisher_id.parse()?;
let participant = self
.remote_participants
.get_mut(&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.0"
+version = "0.4.0"
[[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,10 +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,
@@ -102,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
+);
@@ -6,8 +6,7 @@ CREATE TABLE IF NOT EXISTS "rooms" (
ALTER TABLE "projects"
ADD "room_id" INTEGER REFERENCES rooms (id),
ADD "host_connection_id" INTEGER,
- ADD "host_connection_epoch" UUID,
- DROP COLUMN "unregistered";
+ ADD "host_connection_epoch" UUID;
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
CREATE TABLE "worktrees" (
@@ -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 outdated_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 {
+ ) -> 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(
- room_participant::Column::UserId
- .eq(user_id)
- .and(room_participant::Column::AnsweringConnectionId.is_null()),
- )
+ .filter(filter)
.one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("could not decline call"))?;
- let room_id = participant.room_id;
+ .await?;
- if expected_room_id.map_or(false, |expected_room_id| expected_room_id != room_id) {
- return Err(anyhow!("declining call on unexpected room"))?;
- }
+ 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) {
@@ -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)]
@@ -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);
}
);
@@ -1,19 +1,19 @@
use crate::{
db::{self, NewUserParams, TestDb, UserId},
executor::Executor,
- rpc::{Server, RECONNECT_TIMEOUT},
+ rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState,
};
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;
@@ -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()));
@@ -686,7 +686,7 @@ async fn test_server_restarts(
// The server finishes restarting, cleaning up stale connections.
server.start().await.unwrap();
- deterministic.advance_clock(RECONNECT_TIMEOUT);
+ deterministic.advance_clock(CLEANUP_TIMEOUT);
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -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()));
@@ -806,7 +806,7 @@ async fn test_server_restarts(
// The server finishes restarting, cleaning up stale connections and canceling the
// call to user D because the room has become empty.
server.start().await.unwrap();
- deterministic.advance_clock(RECONNECT_TIMEOUT);
+ deterministic.advance_clock(CLEANUP_TIMEOUT);
assert!(incoming_call_d.next().await.unwrap().is_none());
}
@@ -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)
});
@@ -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(RECONNECT_TIMEOUT);
+ 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,13 +6387,19 @@ 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()),
);
server.start().await.unwrap();
// Advance clock to ensure the server's cleanup task is finished.
- deterministic.advance_clock(RECONNECT_TIMEOUT);
+ deterministic.advance_clock(CLEANUP_TIMEOUT);
Self {
app_state,
server,
@@ -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)
}
})
@@ -6445,7 +6524,7 @@ impl TestServer {
fs: fs.clone(),
build_window_options: Default::default,
initialize_workspace: |_, _, _| unimplemented!(),
- default_item_factory: |_, _| unimplemented!(),
+ dock_default_item_factory: |_, _| unimplemented!(),
});
Project::init(&client);
@@ -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())
@@ -65,6 +70,18 @@ async fn main() -> Result<()> {
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
+ .with_graceful_shutdown(async move {
+ 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,
};
@@ -57,7 +57,8 @@ use tokio::sync::watch;
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
-pub const RECONNECT_TIMEOUT: Duration = rpc::RECEIVE_TIMEOUT;
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
+pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
lazy_static! {
static ref METRIC_CONNECTIONS: IntGauge =
@@ -137,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>,
@@ -167,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(),
@@ -238,98 +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(RECONNECT_TIMEOUT);
+ 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.outdated_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 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();
- }
+ let span = info_span!("start server");
+ let span_enter = span.enter();
+
+ 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();
}
- }
- 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 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 = 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(())
}
- #[cfg(test)]
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);
}
}
@@ -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(());
}
{
@@ -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::{
@@ -474,7 +474,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 +487,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
}
@@ -54,7 +54,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
Default::default(),
0,
project,
- app_state.default_item_factory,
+ app_state.dock_default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -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::{
@@ -465,7 +465,7 @@ impl ContactList {
room.remote_participants()
.iter()
.map(|(peer_id, participant)| StringMatchCandidate {
- id: peer_id.0 as usize,
+ id: peer_id.as_u64() as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}),
@@ -479,7 +479,7 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
- let peer_id = PeerId(mat.candidate_id as u32);
+ let peer_id = PeerId::from_u64(mat.candidate_id as u64);
let participant = &room.remote_participants()[&peer_id];
participant_entries.push(ContactEntry::CallParticipant {
user: participant.user.clone(),
@@ -881,75 +881,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);
-
- 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.);
+ 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);
- 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() },
+ 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() },
+ ),
),
- ),
- 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 });
@@ -199,10 +199,10 @@ macro_rules! query {
use $crate::anyhow::Context;
- self.write(|connection| {
+ self.write(move |connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
- connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+))
+ connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound 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);
@@ -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![
@@ -2421,7 +2423,7 @@ impl Editor {
let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
let excerpt_range = excerpt_range.to_offset(buffer);
buffer
- .edited_ranges_for_transaction(transaction)
+ .edited_ranges_for_transaction::<usize>(transaction)
.all(|range| {
excerpt_range.start <= range.start
&& excerpt_range.end >= range.end
@@ -5883,25 +5885,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 +6097,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 +6114,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);
-
- cx.set_global(Settings::test(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;
- 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,136 @@ 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 buffer = project.update(cx, |project, cx| {
- project.open_buffer_by_id(state.buffer_id, cx)
+ let Some(proto::view::Variant::Editor(_)) = state else { return None };
+ let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
+
+ 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<_>>>()?;
+ let scroll_top_anchor = state
+ .scroll_top_anchor
+ .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+ drop(buffer);
+
if !selections.is_empty() {
editor.set_selections_from_remote(selections, 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 +182,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
@@ -156,18 +223,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
@@ -189,45 +281,98 @@ 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 buffer_ids = message
+ .inserted_excerpts
+ .iter()
+ .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+ .collect::<HashSet<_>>();
+
+ 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(&excerpt_id, buffer_id, selection)
- })
- .collect::<Vec<_>>();
+ let selections = message
+ .selections
+ .into_iter()
+ .filter_map(|selection| deserialize_selection(&multibuffer, selection))
+ .collect::<Vec<_>>();
+ 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() {
- 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,
- );
+ this.set_selections_from_remote(selections, 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 +385,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,
@@ -2746,6 +2821,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 +3159,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);
@@ -3468,7 +3555,7 @@ mod tests {
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 +3582,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 +3613,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 +3622,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 +3837,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 +3950,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 +4038,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));
@@ -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,
})
@@ -32,6 +32,7 @@ lsp = { path = "../lsp" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
+terminal = { path = "../terminal" }
util = { path = "../util" }
aho-corasick = "0.7"
anyhow = "1.0.57"
@@ -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,
@@ -62,6 +61,7 @@ use std::{
},
time::Instant,
};
+use terminal::{Terminal, TerminalBuilder};
use thiserror::Error;
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
@@ -102,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,
@@ -163,7 +163,7 @@ enum ProjectClientState {
#[derive(Clone, Debug)]
pub struct Collaborator {
- pub peer_id: PeerId,
+ pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
}
@@ -184,7 +184,7 @@ pub enum Event {
},
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
- CollaboratorLeft(PeerId),
+ CollaboratorLeft(proto::PeerId),
}
pub enum LanguageServerState {
@@ -554,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);
}
@@ -753,7 +753,7 @@ impl Project {
}
}
- pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
+ pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
&self.collaborators
}
@@ -1193,6 +1193,34 @@ impl Project {
!self.is_local()
}
+ pub fn create_terminal(
+ &mut self,
+ working_directory: Option<PathBuf>,
+ window_id: usize,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<ModelHandle<Terminal>> {
+ if self.is_remote() {
+ return Err(anyhow!(
+ "creating terminals as a guest is not supported yet"
+ ));
+ } else {
+ let settings = cx.global::<Settings>();
+ let shell = settings.terminal_shell();
+ let envs = settings.terminal_env();
+ let scroll = settings.terminal_scroll();
+
+ TerminalBuilder::new(
+ working_directory.clone(),
+ shell,
+ envs,
+ settings.terminal_overrides.blinking.clone(),
+ scroll,
+ window_id,
+ )
+ .map(|builder| cx.add_model(|cx| builder.subscribe(cx)))
+ }
+ }
+
pub fn create_buffer(
&mut self,
text: &str,
@@ -4576,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);
@@ -4593,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)
@@ -5460,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 {
@@ -5516,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();
@@ -5534,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)),
})?;
@@ -5551,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,
@@ -6007,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,
- }
+ })
}
}
@@ -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,55 @@ 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;
+ EditorAnchor scroll_top_anchor = 4;
+ float scroll_x = 5;
+ float scroll_y = 6;
}
}
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;
+ EditorAnchor scroll_top_anchor = 5;
+ float scroll_x = 6;
+ float scroll_y = 7;
}
}
message Collaborator {
- uint32 peer_id = 1;
+ PeerId peer_id = 1;
uint32 replica_id = 2;
uint64 user_id = 3;
}
@@ -913,21 +927,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 +946,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,16 @@
-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::str::FromStr;
use std::{
+ cmp,
fmt::Debug,
- io,
+ io, iter, mem,
time::{Duration, SystemTime, UNIX_EPOCH},
};
@@ -21,7 +23,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 +76,66 @@ 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)
+ }
+}
+
+impl FromStr for PeerId {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut components = s.split('/');
+ let owner_id = components
+ .next()
+ .ok_or_else(|| anyhow!("invalid peer id {:?}", s))?
+ .parse()?;
+ let id = components
+ .next()
+ .ok_or_else(|| anyhow!("invalid peer id {:?}", s))?
+ .parse()?;
+ Ok(PeerId { owner_id, id })
+ }
+}
+
messages!(
(Ack, Foreground),
(AddProjectCollaborator, Foreground),
@@ -477,4 +539,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 = 42;
@@ -402,7 +402,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 +419,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();
@@ -199,7 +199,7 @@ impl Default for Shell {
}
}
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
@@ -221,6 +221,12 @@ pub enum WorkingDirectory {
Always { directory: String },
}
+impl Default for WorkingDirectory {
+ fn default() -> Self {
+ Self::CurrentProjectDirectory
+ }
+}
+
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DockAnchor {
@@ -473,6 +479,32 @@ impl Settings {
})
}
+ fn terminal_setting<F, R: Default + Clone>(&self, f: F) -> R
+ where
+ F: Fn(&TerminalSettings) -> Option<&R>,
+ {
+ f(&self.terminal_overrides)
+ .or_else(|| f(&self.terminal_defaults))
+ .cloned()
+ .unwrap_or_else(|| R::default())
+ }
+
+ pub fn terminal_scroll(&self) -> AlternateScroll {
+ self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.as_ref())
+ }
+
+ pub fn terminal_shell(&self) -> Shell {
+ self.terminal_setting(|terminal_setting| terminal_setting.shell.as_ref())
+ }
+
+ pub fn terminal_env(&self) -> HashMap<String, String> {
+ self.terminal_setting(|terminal_setting| terminal_setting.env.as_ref())
+ }
+
+ pub fn terminal_strategy(&self) -> WorkingDirectory {
+ self.terminal_setting(|terminal_setting| terminal_setting.working_directory.as_ref())
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
@@ -7,17 +7,13 @@ edition = "2021"
path = "src/terminal.rs"
doctest = false
+
[dependencies]
-context_menu = { path = "../context_menu" }
-editor = { path = "../editor" }
-language = { path = "../language" }
gpui = { path = "../gpui" }
-project = { path = "../project" }
settings = { path = "../settings" }
+db = { path = "../db" }
theme = { path = "../theme" }
util = { path = "../util" }
-workspace = { path = "../workspace" }
-db = { path = "../db" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
smallvec = { version = "1.6", features = ["union"] }
@@ -34,11 +30,5 @@ thiserror = "1.0"
lazy_static = "1.4.0"
serde = { version = "1.0", features = ["derive"] }
-
-
[dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"]}
-project = { path = "../project", features = ["test-support"]}
-workspace = { path = "../workspace", features = ["test-support"] }
rand = "0.8.5"
@@ -1,8 +1,5 @@
pub mod mappings;
-mod persistence;
-pub mod terminal_container_view;
-pub mod terminal_element;
-pub mod terminal_view;
+pub use alacritty_terminal;
use alacritty_terminal::{
ansi::{ClearMode, Handler},
@@ -33,11 +30,9 @@ use mappings::mouse::{
alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
};
-use persistence::TERMINAL_CONNECTION;
use procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::ResultExt;
-use workspace::{ItemId, WorkspaceId};
use std::{
cmp::min,
@@ -57,8 +52,7 @@ use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
- AppContext, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent,
- MutableAppContext, Task,
+ ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
};
use crate::mappings::{
@@ -67,12 +61,6 @@ use crate::mappings::{
};
use lazy_static::lazy_static;
-///Initialize and register all of our action handlers
-pub fn init(cx: &mut MutableAppContext) {
- terminal_view::init(cx);
- terminal_container_view::init(cx);
-}
-
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
@@ -128,10 +116,10 @@ impl EventListener for ZedListener {
#[derive(Clone, Copy, Debug)]
pub struct TerminalSize {
- cell_width: f32,
- line_height: f32,
- height: f32,
- width: f32,
+ pub cell_width: f32,
+ pub line_height: f32,
+ pub height: f32,
+ pub width: f32,
}
impl TerminalSize {
@@ -210,7 +198,7 @@ impl Dimensions for TerminalSize {
#[derive(Error, Debug)]
pub struct TerminalError {
pub directory: Option<PathBuf>,
- pub shell: Option<Shell>,
+ pub shell: Shell,
pub source: std::io::Error,
}
@@ -238,24 +226,20 @@ impl TerminalError {
})
}
- pub fn shell_to_string(&self) -> Option<String> {
- self.shell.as_ref().map(|shell| match shell {
+ pub fn shell_to_string(&self) -> String {
+ match &self.shell {
Shell::System => "<system shell>".to_string(),
Shell::Program(p) => p.to_string(),
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
- })
+ }
}
pub fn fmt_shell(&self) -> String {
- self.shell
- .clone()
- .map(|shell| match shell {
- Shell::System => "<system defined shell>".to_string(),
-
- Shell::Program(s) => s,
- Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
- })
- .unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
+ match &self.shell {
+ Shell::System => "<system defined shell>".to_string(),
+ Shell::Program(s) => s.to_string(),
+ Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+ }
}
}
@@ -280,20 +264,18 @@ pub struct TerminalBuilder {
impl TerminalBuilder {
pub fn new(
working_directory: Option<PathBuf>,
- shell: Option<Shell>,
- env: Option<HashMap<String, String>>,
+ shell: Shell,
+ mut env: HashMap<String, String>,
blink_settings: Option<TerminalBlink>,
- alternate_scroll: &AlternateScroll,
+ alternate_scroll: AlternateScroll,
window_id: usize,
- item_id: ItemId,
- workspace_id: WorkspaceId,
) -> Result<TerminalBuilder> {
let pty_config = {
- let alac_shell = shell.clone().and_then(|shell| match shell {
+ let alac_shell = match shell.clone() {
Shell::System => None,
Shell::Program(program) => Some(Program::Just(program)),
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
- });
+ };
PtyConfig {
shell: alac_shell,
@@ -302,10 +284,9 @@ impl TerminalBuilder {
}
};
- let mut env = env.unwrap_or_default();
-
//TODO: Properly set the current locale,
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
+ env.insert("ZED_TERM".to_string(), true.to_string());
let alac_scrolling = Scrolling::default();
// alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
@@ -391,8 +372,6 @@ impl TerminalBuilder {
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
- workspace_id,
- item_id,
};
Ok(TerminalBuilder {
@@ -464,9 +443,9 @@ impl TerminalBuilder {
}
#[derive(Debug, Clone)]
-struct IndexedCell {
- point: Point,
- cell: Cell,
+pub struct IndexedCell {
+ pub point: Point,
+ pub cell: Cell,
}
impl Deref for IndexedCell {
@@ -478,17 +457,18 @@ impl Deref for IndexedCell {
}
}
+// TODO: Un-pub
#[derive(Clone)]
pub struct TerminalContent {
- cells: Vec<IndexedCell>,
- mode: TermMode,
- display_offset: usize,
- selection_text: Option<String>,
- selection: Option<SelectionRange>,
- cursor: RenderableCursor,
- cursor_char: char,
- size: TerminalSize,
- last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+ pub cells: Vec<IndexedCell>,
+ pub mode: TermMode,
+ pub display_offset: usize,
+ pub selection_text: Option<String>,
+ pub selection: Option<SelectionRange>,
+ pub cursor: RenderableCursor,
+ pub cursor_char: char,
+ pub size: TerminalSize,
+ pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
}
impl Default for TerminalContent {
@@ -525,19 +505,17 @@ pub struct Terminal {
/// This is only used for terminal hyperlink checking
last_mouse_position: Option<Vector2F>,
pub matches: Vec<RangeInclusive<Point>>,
- last_content: TerminalContent,
+ pub last_content: TerminalContent,
last_synced: Instant,
sync_task: Option<Task<()>>,
- selection_head: Option<Point>,
- breadcrumb_text: String,
+ pub selection_head: Option<Point>,
+ pub breadcrumb_text: String,
shell_pid: u32,
shell_fd: u32,
- foreground_process_info: Option<LocalProcessInfo>,
+ pub foreground_process_info: Option<LocalProcessInfo>,
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
- workspace_id: WorkspaceId,
- item_id: ItemId,
}
impl Terminal {
@@ -578,20 +556,6 @@ impl Terminal {
if self.update_process_info() {
cx.emit(Event::TitleChanged);
-
- if let Some(foreground_info) = &self.foreground_process_info {
- let cwd = foreground_info.cwd.clone();
- let item_id = self.item_id;
- let workspace_id = self.workspace_id;
- cx.background()
- .spawn(async move {
- TERMINAL_CONNECTION
- .save_working_directory(item_id, workspace_id, cwd)
- .await
- .log_err();
- })
- .detach();
- }
}
}
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
@@ -1194,42 +1158,13 @@ impl Terminal {
}
}
- pub fn set_workspace_id(&mut self, id: WorkspaceId, cx: &AppContext) {
- let old_workspace_id = self.workspace_id;
- let item_id = self.item_id;
- cx.background()
- .spawn(async move {
- TERMINAL_CONNECTION
- .update_workspace_id(id, old_workspace_id, item_id)
- .await
- .log_err()
- })
- .detach();
-
- self.workspace_id = id;
- }
-
pub fn find_matches(
&mut self,
- query: project::search::SearchQuery,
+ searcher: RegexSearch,
cx: &mut ModelContext<Self>,
) -> Task<Vec<RangeInclusive<Point>>> {
let term = self.term.clone();
cx.background().spawn(async move {
- let searcher = match query {
- project::search::SearchQuery::Text { query, .. } => {
- RegexSearch::new(query.as_ref())
- }
- project::search::SearchQuery::Regex { query, .. } => {
- RegexSearch::new(query.as_ref())
- }
- };
-
- if searcher.is_err() {
- return Vec::new();
- }
- let searcher = searcher.unwrap();
-
let term = term.lock();
all_search_matches(&term, &searcher).collect()
@@ -1326,14 +1261,14 @@ fn open_uri(uri: &str) -> Result<(), std::io::Error> {
#[cfg(test)]
mod tests {
+ use alacritty_terminal::{
+ index::{Column, Line, Point},
+ term::cell::Cell,
+ };
use gpui::geometry::vector::vec2f;
- use rand::{thread_rng, Rng};
-
- use crate::content_index_for_mouse;
-
- use self::terminal_test_context::TerminalTestContext;
+ use rand::{rngs::ThreadRng, thread_rng, Rng};
- pub mod terminal_test_context;
+ use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
#[test]
fn test_mouse_to_cell() {
@@ -1350,7 +1285,7 @@ mod tests {
width: cell_size * (viewport_cells as f32),
};
- let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+ let (content, cells) = create_terminal_content(size, &mut rng);
for i in 0..(viewport_cells - 1) {
let i = i as usize;
@@ -1386,7 +1321,7 @@ mod tests {
width: 100.,
};
- let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+ let (content, cells) = create_terminal_content(size, &mut rng);
assert_eq!(
content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c,
@@ -1397,4 +1332,37 @@ mod tests {
cells[9][9]
);
}
+
+ fn create_terminal_content(
+ size: TerminalSize,
+ rng: &mut ThreadRng,
+ ) -> (TerminalContent, Vec<Vec<char>>) {
+ let mut ic = Vec::new();
+ let mut cells = Vec::new();
+
+ for row in 0..((size.height() / size.line_height()) as usize) {
+ let mut row_vec = Vec::new();
+ for col in 0..((size.width() / size.cell_width()) as usize) {
+ let cell_char = rng.gen();
+ ic.push(IndexedCell {
+ point: Point::new(Line(row as i32), Column(col)),
+ cell: Cell {
+ c: cell_char,
+ ..Default::default()
+ },
+ });
+ row_vec.push(cell_char)
+ }
+ cells.push(row_vec)
+ }
+
+ (
+ TerminalContent {
+ cells: ic,
+ size,
+ ..Default::default()
+ },
+ cells,
+ )
+ }
}
@@ -1,711 +0,0 @@
-use crate::persistence::TERMINAL_CONNECTION;
-use crate::terminal_view::TerminalView;
-use crate::{Event, TerminalBuilder, TerminalError};
-
-use alacritty_terminal::index::Point;
-use dirs::home_dir;
-use gpui::{
- actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
- View, ViewContext, ViewHandle, WeakViewHandle,
-};
-use util::{truncate_and_trailoff, ResultExt};
-use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
-use workspace::{
- item::{Item, ItemEvent},
- ToolbarItemLocation, Workspace,
-};
-use workspace::{register_deserializable_item, Pane, WorkspaceId};
-
-use project::{LocalWorktree, Project, ProjectPath};
-use settings::{AlternateScroll, Settings, WorkingDirectory};
-use smallvec::SmallVec;
-use std::ops::RangeInclusive;
-use std::path::{Path, PathBuf};
-
-use crate::terminal_element::TerminalElement;
-
-actions!(terminal, [DeployModal]);
-
-pub fn init(cx: &mut MutableAppContext) {
- cx.add_action(TerminalContainer::deploy);
-
- register_deserializable_item::<TerminalContainer>(cx);
-}
-
-//Make terminal view an enum, that can give you views for the error and non-error states
-//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
-//Bubble up to deploy(_modal)() calls
-
-pub enum TerminalContainerContent {
- Connected(ViewHandle<TerminalView>),
- Error(ViewHandle<ErrorView>),
-}
-
-impl TerminalContainerContent {
- fn handle(&self) -> AnyViewHandle {
- match self {
- Self::Connected(handle) => handle.into(),
- Self::Error(handle) => handle.into(),
- }
- }
-}
-
-pub struct TerminalContainer {
- pub content: TerminalContainerContent,
- associated_directory: Option<PathBuf>,
-}
-
-pub struct ErrorView {
- error: TerminalError,
-}
-
-impl Entity for TerminalContainer {
- type Event = Event;
-}
-
-impl Entity for ErrorView {
- type Event = Event;
-}
-
-impl TerminalContainer {
- ///Create a new Terminal in the current working directory or the user's home directory
- pub fn deploy(
- workspace: &mut Workspace,
- _: &workspace::NewTerminal,
- cx: &mut ViewContext<Workspace>,
- ) {
- let strategy = cx
- .global::<Settings>()
- .terminal_overrides
- .working_directory
- .clone()
- .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
- let working_directory = get_working_directory(workspace, cx, strategy);
- let view = cx.add_view(|cx| {
- TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
- });
- workspace.add_item(Box::new(view), cx);
- }
-
- ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
- pub fn new(
- working_directory: Option<PathBuf>,
- modal: bool,
- workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let settings = cx.global::<Settings>();
- let shell = settings.terminal_overrides.shell.clone();
- let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
-
- //TODO: move this pattern to settings
- let scroll = settings
- .terminal_overrides
- .alternate_scroll
- .as_ref()
- .unwrap_or(
- settings
- .terminal_defaults
- .alternate_scroll
- .as_ref()
- .unwrap_or_else(|| &AlternateScroll::On),
- );
-
- let content = match TerminalBuilder::new(
- working_directory.clone(),
- shell,
- envs,
- settings.terminal_overrides.blinking.clone(),
- scroll,
- cx.window_id(),
- cx.view_id(),
- workspace_id,
- ) {
- Ok(terminal) => {
- let terminal = cx.add_model(|cx| terminal.subscribe(cx));
- let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
-
- cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
- .detach();
- TerminalContainerContent::Connected(view)
- }
- Err(error) => {
- let view = cx.add_view(|_| ErrorView {
- error: error.downcast::<TerminalError>().unwrap(),
- });
- TerminalContainerContent::Error(view)
- }
- };
-
- TerminalContainer {
- content,
- associated_directory: working_directory,
- }
- }
-
- fn connected(&self) -> Option<ViewHandle<TerminalView>> {
- match &self.content {
- TerminalContainerContent::Connected(vh) => Some(vh.clone()),
- TerminalContainerContent::Error(_) => None,
- }
- }
-}
-
-impl View for TerminalContainer {
- fn ui_name() -> &'static str {
- "Terminal"
- }
-
- fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
- match &self.content {
- TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx),
- TerminalContainerContent::Error(error) => ChildView::new(error, cx),
- }
- .boxed()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- cx.focus(self.content.handle());
- }
- }
-}
-
-impl View for ErrorView {
- fn ui_name() -> &'static str {
- "Terminal Error"
- }
-
- fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
- let settings = cx.global::<Settings>();
- let style = TerminalElement::make_text_style(cx.font_cache(), settings);
-
- //TODO:
- //We want markdown style highlighting so we can format the program and working directory with ``
- //We want a max-width of 75% with word-wrap
- //We want to be able to select the text
- //Want to be able to scroll if the error message is massive somehow (resiliency)
-
- let program_text = {
- match self.error.shell_to_string() {
- Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
- None => "No program specified".to_string(),
- }
- };
-
- let directory_text = {
- match self.error.directory.as_ref() {
- Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
- None => "No working directory specified".to_string(),
- }
- };
-
- let error_text = self.error.source.to_string();
-
- Flex::column()
- .with_child(
- Text::new("Failed to open the terminal.".to_string(), style.clone())
- .contained()
- .boxed(),
- )
- .with_child(Text::new(program_text, style.clone()).contained().boxed())
- .with_child(Text::new(directory_text, style.clone()).contained().boxed())
- .with_child(Text::new(error_text, style).contained().boxed())
- .aligned()
- .boxed()
- }
-}
-
-impl Item for TerminalContainer {
- fn tab_content(
- &self,
- _detail: Option<usize>,
- tab_theme: &theme::Tab,
- cx: &gpui::AppContext,
- ) -> ElementBox {
- let title = match &self.content {
- TerminalContainerContent::Connected(connected) => connected
- .read(cx)
- .handle()
- .read(cx)
- .foreground_process_info
- .as_ref()
- .map(|fpi| {
- format!(
- "{} — {}",
- truncate_and_trailoff(
- &fpi.cwd
- .file_name()
- .map(|name| name.to_string_lossy().to_string())
- .unwrap_or_default(),
- 25
- ),
- truncate_and_trailoff(
- &{
- format!(
- "{}{}",
- fpi.name,
- if fpi.argv.len() >= 1 {
- format!(" {}", (&fpi.argv[1..]).join(" "))
- } else {
- "".to_string()
- }
- )
- },
- 25
- )
- )
- })
- .unwrap_or_else(|| "Terminal".to_string()),
- TerminalContainerContent::Error(_) => "Terminal".to_string(),
- };
-
- Flex::row()
- .with_child(
- Label::new(title, tab_theme.label.clone())
- .aligned()
- .contained()
- .boxed(),
- )
- .boxed()
- }
-
- fn clone_on_split(
- &self,
- workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<Self> {
- //From what I can tell, there's no way to tell the current working
- //Directory of the terminal from outside the shell. There might be
- //solutions to this, but they are non-trivial and require more IPC
- Some(TerminalContainer::new(
- self.associated_directory.clone(),
- false,
- workspace_id,
- cx,
- ))
- }
-
- fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
- None
- }
-
- fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
- SmallVec::new()
- }
-
- fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
- false
- }
-
- fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
- fn can_save(&self, _cx: &gpui::AppContext) -> bool {
- false
- }
-
- fn save(
- &mut self,
- _project: gpui::ModelHandle<Project>,
- _cx: &mut ViewContext<Self>,
- ) -> gpui::Task<gpui::anyhow::Result<()>> {
- unreachable!("save should not have been called");
- }
-
- fn save_as(
- &mut self,
- _project: gpui::ModelHandle<Project>,
- _abs_path: std::path::PathBuf,
- _cx: &mut ViewContext<Self>,
- ) -> gpui::Task<gpui::anyhow::Result<()>> {
- unreachable!("save_as should not have been called");
- }
-
- fn reload(
- &mut self,
- _project: gpui::ModelHandle<Project>,
- _cx: &mut ViewContext<Self>,
- ) -> gpui::Task<gpui::anyhow::Result<()>> {
- gpui::Task::ready(Ok(()))
- }
-
- fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- connected.read(cx).has_bell()
- } else {
- false
- }
- }
-
- fn has_conflict(&self, _cx: &AppContext) -> bool {
- false
- }
-
- fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
- match event {
- Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs],
- Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab],
- Event::CloseTerminal => vec![ItemEvent::CloseItem],
- _ => vec![],
- }
- }
-
- fn breadcrumb_location(&self) -> ToolbarItemLocation {
- if self.connected().is_some() {
- ToolbarItemLocation::PrimaryLeft { flex: None }
- } else {
- ToolbarItemLocation::Hidden
- }
- }
-
- fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
- let connected = self.connected()?;
-
- Some(vec![Text::new(
- connected
- .read(cx)
- .terminal()
- .read(cx)
- .breadcrumb_text
- .to_string(),
- theme.breadcrumbs.text.clone(),
- )
- .boxed()])
- }
-
- fn serialized_item_kind() -> Option<&'static str> {
- Some("Terminal")
- }
-
- fn deserialize(
- _project: ModelHandle<Project>,
- _workspace: WeakViewHandle<Workspace>,
- workspace_id: workspace::WorkspaceId,
- item_id: workspace::ItemId,
- cx: &mut ViewContext<Pane>,
- ) -> Task<anyhow::Result<ViewHandle<Self>>> {
- let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id);
- Task::ready(Ok(cx.add_view(|cx| {
- TerminalContainer::new(
- working_directory.log_err().flatten(),
- false,
- workspace_id,
- cx,
- )
- })))
- }
-
- fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
- if let Some(connected) = self.connected() {
- let id = workspace.database_id();
- let terminal_handle = connected.read(cx).terminal().clone();
- terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx))
- }
- }
-}
-
-impl SearchableItem for TerminalContainer {
- type Match = RangeInclusive<Point>;
-
- fn supported_options() -> SearchOptions {
- SearchOptions {
- case: false,
- word: false,
- regex: false,
- }
- }
-
- /// Convert events raised by this item into search-relevant events (if applicable)
- fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
- match event {
- Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
- Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
- _ => None,
- }
- }
-
- /// Clear stored matches
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- let terminal = connected.read(cx).terminal().clone();
- terminal.update(cx, |term, _| term.matches.clear())
- }
- }
-
- /// Store matches returned from find_matches somewhere for rendering
- fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- let terminal = connected.read(cx).terminal().clone();
- terminal.update(cx, |term, _| term.matches = matches)
- }
- }
-
- /// Return the selection content to pre-load into this search
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- let terminal = connected.read(cx).terminal().clone();
- terminal
- .read(cx)
- .last_content
- .selection_text
- .clone()
- .unwrap_or_default()
- } else {
- Default::default()
- }
- }
-
- /// Focus match at given index into the Vec of matches
- fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- let terminal = connected.read(cx).terminal().clone();
- terminal.update(cx, |term, _| term.activate_match(index));
- cx.notify();
- }
- }
-
- /// Get all of the matches for this query, should be done on the background
- fn find_matches(
- &mut self,
- query: project::search::SearchQuery,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Self::Match>> {
- if let TerminalContainerContent::Connected(connected) = &self.content {
- let terminal = connected.read(cx).terminal().clone();
- terminal.update(cx, |term, cx| term.find_matches(query, cx))
- } else {
- Task::ready(Vec::new())
- }
- }
-
- /// Reports back to the search toolbar what the active match should be (the selection)
- fn active_match_index(
- &mut self,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) -> Option<usize> {
- let connected = self.connected();
- // Selection head might have a value if there's a selection that isn't
- // associated with a match. Therefore, if there are no matches, we should
- // report None, no matter the state of the terminal
- let res = if matches.len() > 0 && connected.is_some() {
- if let Some(selection_head) = connected
- .unwrap()
- .read(cx)
- .terminal()
- .read(cx)
- .selection_head
- {
- // If selection head is contained in a match. Return that match
- if let Some(ix) = matches
- .iter()
- .enumerate()
- .find(|(_, search_match)| {
- search_match.contains(&selection_head)
- || search_match.start() > &selection_head
- })
- .map(|(ix, _)| ix)
- {
- Some(ix)
- } else {
- // If no selection after selection head, return the last match
- Some(matches.len().saturating_sub(1))
- }
- } else {
- // Matches found but no active selection, return the first last one (closest to cursor)
- Some(matches.len().saturating_sub(1))
- }
- } else {
- None
- };
-
- res
- }
-}
-
-///Get's the working directory for the given workspace, respecting the user's settings.
-pub fn get_working_directory(
- workspace: &Workspace,
- cx: &AppContext,
- strategy: WorkingDirectory,
-) -> Option<PathBuf> {
- let res = match strategy {
- WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
- .or_else(|| first_project_directory(workspace, cx)),
- WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
- WorkingDirectory::AlwaysHome => None,
- WorkingDirectory::Always { directory } => {
- shellexpand::full(&directory) //TODO handle this better
- .ok()
- .map(|dir| Path::new(&dir.to_string()).to_path_buf())
- .filter(|dir| dir.is_dir())
- }
- };
- res.or_else(home_dir)
-}
-
-///Get's the first project's home directory, or the home directory
-fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
- workspace
- .worktrees(cx)
- .next()
- .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
- .and_then(get_path_from_wt)
-}
-
-///Gets the intuitively correct working directory from the given workspace
-///If there is an active entry for this project, returns that entry's worktree root.
-///If there's no active entry but there is a worktree, returns that worktrees root.
-///If either of these roots are files, or if there are any other query failures,
-/// returns the user's home directory
-fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
- let project = workspace.project().read(cx);
-
- project
- .active_entry()
- .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
- .or_else(|| workspace.worktrees(cx).next())
- .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
- .and_then(get_path_from_wt)
-}
-
-fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
- wt.root_entry()
- .filter(|re| re.is_dir())
- .map(|_| wt.abs_path().to_path_buf())
-}
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
- use gpui::TestAppContext;
-
- use std::path::Path;
-
- use crate::tests::terminal_test_context::TerminalTestContext;
-
- ///Working directory calculation tests
-
- ///No Worktrees in project -> home_dir()
- #[gpui::test]
- async fn no_worktree(cx: &mut TestAppContext) {
- //Setup variables
- let mut cx = TerminalTestContext::new(cx);
- let (project, workspace) = cx.blank_workspace().await;
- //Test
- cx.cx.read(|cx| {
- let workspace = workspace.read(cx);
- let active_entry = project.read(cx).active_entry();
-
- //Make sure enviroment is as expeted
- assert!(active_entry.is_none());
- assert!(workspace.worktrees(cx).next().is_none());
-
- let res = current_project_directory(workspace, cx);
- assert_eq!(res, None);
- let res = first_project_directory(workspace, cx);
- assert_eq!(res, None);
- });
- }
-
- ///No active entry, but a worktree, worktree is a file -> home_dir()
- #[gpui::test]
- async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
- //Setup variables
-
- let mut cx = TerminalTestContext::new(cx);
- let (project, workspace) = cx.blank_workspace().await;
- cx.create_file_wt(project.clone(), "/root.txt").await;
-
- cx.cx.read(|cx| {
- let workspace = workspace.read(cx);
- let active_entry = project.read(cx).active_entry();
-
- //Make sure enviroment is as expeted
- assert!(active_entry.is_none());
- assert!(workspace.worktrees(cx).next().is_some());
-
- let res = current_project_directory(workspace, cx);
- assert_eq!(res, None);
- let res = first_project_directory(workspace, cx);
- assert_eq!(res, None);
- });
- }
-
- //No active entry, but a worktree, worktree is a folder -> worktree_folder
- #[gpui::test]
- async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
- //Setup variables
- let mut cx = TerminalTestContext::new(cx);
- let (project, workspace) = cx.blank_workspace().await;
- let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await;
-
- //Test
- cx.cx.update(|cx| {
- let workspace = workspace.read(cx);
- let active_entry = project.read(cx).active_entry();
-
- assert!(active_entry.is_none());
- assert!(workspace.worktrees(cx).next().is_some());
-
- let res = current_project_directory(workspace, cx);
- assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
- let res = first_project_directory(workspace, cx);
- assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
- });
- }
-
- //Active entry with a work tree, worktree is a file -> home_dir()
- #[gpui::test]
- async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
- //Setup variables
- let mut cx = TerminalTestContext::new(cx);
- let (project, workspace) = cx.blank_workspace().await;
- let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
- let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await;
- cx.insert_active_entry_for(wt2, entry2, project.clone());
-
- //Test
- cx.cx.update(|cx| {
- let workspace = workspace.read(cx);
- let active_entry = project.read(cx).active_entry();
-
- assert!(active_entry.is_some());
-
- let res = current_project_directory(workspace, cx);
- assert_eq!(res, None);
- let res = first_project_directory(workspace, cx);
- assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
- });
- }
-
- //Active entry, with a worktree, worktree is a folder -> worktree_folder
- #[gpui::test]
- async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
- //Setup variables
- let mut cx = TerminalTestContext::new(cx);
- let (project, workspace) = cx.blank_workspace().await;
- let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
- let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await;
- cx.insert_active_entry_for(wt2, entry2, project.clone());
-
- //Test
- cx.cx.update(|cx| {
- let workspace = workspace.read(cx);
- let active_entry = project.read(cx).active_entry();
-
- assert!(active_entry.is_some());
-
- let res = current_project_directory(workspace, cx);
- assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
- let res = first_project_directory(workspace, cx);
- assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
- });
- }
-}
@@ -1,471 +0,0 @@
-use std::{ops::RangeInclusive, time::Duration};
-
-use alacritty_terminal::{index::Point, term::TermMode};
-use context_menu::{ContextMenu, ContextMenuItem};
-use gpui::{
- actions,
- elements::{AnchorCorner, ChildView, ParentElement, Stack},
- geometry::vector::Vector2F,
- impl_actions, impl_internal_actions,
- keymap::Keystroke,
- AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
- View, ViewContext, ViewHandle,
-};
-use serde::Deserialize;
-use settings::{Settings, TerminalBlink};
-use smol::Timer;
-use util::ResultExt;
-use workspace::pane;
-
-use crate::{terminal_element::TerminalElement, Event, Terminal};
-
-const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
-
-///Event to transmit the scroll from the element to the view
-#[derive(Clone, Debug, PartialEq)]
-pub struct ScrollTerminal(pub i32);
-
-#[derive(Clone, PartialEq)]
-pub struct DeployContextMenu {
- pub position: Vector2F,
-}
-
-#[derive(Clone, Default, Deserialize, PartialEq)]
-pub struct SendText(String);
-
-#[derive(Clone, Default, Deserialize, PartialEq)]
-pub struct SendKeystroke(String);
-
-actions!(
- terminal,
- [Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
-);
-
-impl_actions!(terminal, [SendText, SendKeystroke]);
-
-impl_internal_actions!(project_panel, [DeployContextMenu]);
-
-pub fn init(cx: &mut MutableAppContext) {
- //Useful terminal views
- cx.add_action(TerminalView::send_text);
- cx.add_action(TerminalView::send_keystroke);
- cx.add_action(TerminalView::deploy_context_menu);
- cx.add_action(TerminalView::copy);
- cx.add_action(TerminalView::paste);
- cx.add_action(TerminalView::clear);
- cx.add_action(TerminalView::show_character_palette);
-}
-
-///A terminal view, maintains the PTY's file handles and communicates with the terminal
-pub struct TerminalView {
- terminal: ModelHandle<Terminal>,
- has_new_content: bool,
- //Currently using iTerm bell, show bell emoji in tab until input is received
- has_bell: bool,
- // Only for styling purposes. Doesn't effect behavior
- modal: bool,
- context_menu: ViewHandle<ContextMenu>,
- blink_state: bool,
- blinking_on: bool,
- blinking_paused: bool,
- blink_epoch: usize,
-}
-
-impl Entity for TerminalView {
- type Event = Event;
-}
-
-impl TerminalView {
- pub fn from_terminal(
- terminal: ModelHandle<Terminal>,
- modal: bool,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
- cx.subscribe(&terminal, |this, _, event, cx| match event {
- Event::Wakeup => {
- if !cx.is_self_focused() {
- this.has_new_content = true;
- cx.notify();
- }
- cx.emit(Event::Wakeup);
- }
- Event::Bell => {
- this.has_bell = true;
- cx.emit(Event::Wakeup);
- }
- Event::BlinkChanged => this.blinking_on = !this.blinking_on,
- _ => cx.emit(*event),
- })
- .detach();
-
- Self {
- terminal,
- has_new_content: true,
- has_bell: false,
- modal,
- context_menu: cx.add_view(ContextMenu::new),
- blink_state: true,
- blinking_on: false,
- blinking_paused: false,
- blink_epoch: 0,
- }
- }
-
- pub fn handle(&self) -> ModelHandle<Terminal> {
- self.terminal.clone()
- }
-
- pub fn has_new_content(&self) -> bool {
- self.has_new_content
- }
-
- pub fn has_bell(&self) -> bool {
- self.has_bell
- }
-
- pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
- self.has_bell = false;
- cx.emit(Event::Wakeup);
- }
-
- pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
- let menu_entries = vec![
- ContextMenuItem::item("Clear", Clear),
- ContextMenuItem::item("Close", pane::CloseActiveItem),
- ];
-
- self.context_menu.update(cx, |menu, cx| {
- menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
- });
-
- cx.notify();
- }
-
- fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
- if !self
- .terminal
- .read(cx)
- .last_content
- .mode
- .contains(TermMode::ALT_SCREEN)
- {
- cx.show_character_palette();
- } else {
- self.terminal.update(cx, |term, cx| {
- term.try_keystroke(
- &Keystroke::parse("ctrl-cmd-space").unwrap(),
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
- )
- });
- }
- }
-
- fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
- self.terminal.update(cx, |term, _| term.clear());
- cx.notify();
- }
-
- pub fn should_show_cursor(
- &self,
- focused: bool,
- cx: &mut gpui::RenderContext<'_, Self>,
- ) -> bool {
- //Don't blink the cursor when not focused, blinking is disabled, or paused
- if !focused
- || !self.blinking_on
- || self.blinking_paused
- || self
- .terminal
- .read(cx)
- .last_content
- .mode
- .contains(TermMode::ALT_SCREEN)
- {
- return true;
- }
-
- let setting = {
- let settings = cx.global::<Settings>();
- settings
- .terminal_overrides
- .blinking
- .clone()
- .unwrap_or(TerminalBlink::TerminalControlled)
- };
-
- match setting {
- //If the user requested to never blink, don't blink it.
- TerminalBlink::Off => true,
- //If the terminal is controlling it, check terminal mode
- TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
- }
- }
-
- fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
- if epoch == self.blink_epoch && !self.blinking_paused {
- self.blink_state = !self.blink_state;
- cx.notify();
-
- let epoch = self.next_blink_epoch();
- cx.spawn(|this, mut cx| {
- let this = this.downgrade();
- async move {
- Timer::after(CURSOR_BLINK_INTERVAL).await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
- }
- }
- })
- .detach();
- }
- }
-
- pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
- self.blink_state = true;
- cx.notify();
-
- let epoch = self.next_blink_epoch();
- cx.spawn(|this, mut cx| {
- let this = this.downgrade();
- async move {
- Timer::after(CURSOR_BLINK_INTERVAL).await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
- }
- }
- })
- .detach();
- }
-
- pub fn find_matches(
- &mut self,
- query: project::search::SearchQuery,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<RangeInclusive<Point>>> {
- self.terminal
- .update(cx, |term, cx| term.find_matches(query, cx))
- }
-
- pub fn terminal(&self) -> &ModelHandle<Terminal> {
- &self.terminal
- }
-
- fn next_blink_epoch(&mut self) -> usize {
- self.blink_epoch += 1;
- self.blink_epoch
- }
-
- fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
- if epoch == self.blink_epoch {
- self.blinking_paused = false;
- self.blink_cursors(epoch, cx);
- }
- }
-
- ///Attempt to paste the clipboard into the terminal
- fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
- self.terminal.update(cx, |term, _| term.copy())
- }
-
- ///Attempt to paste the clipboard into the terminal
- fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
- if let Some(item) = cx.read_from_clipboard() {
- self.terminal
- .update(cx, |terminal, _cx| terminal.paste(item.text()));
- }
- }
-
- fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
- self.clear_bel(cx);
- self.terminal.update(cx, |term, _| {
- term.input(text.0.to_string());
- });
- }
-
- fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
- if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
- self.clear_bel(cx);
- self.terminal.update(cx, |term, cx| {
- term.try_keystroke(
- &keystroke,
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
- );
- });
- }
- }
-}
-
-impl View for TerminalView {
- fn ui_name() -> &'static str {
- "Terminal"
- }
-
- fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
- let terminal_handle = self.terminal.clone().downgrade();
-
- let self_id = cx.view_id();
- let focused = cx
- .focused_view_id(cx.window_id())
- .filter(|view_id| *view_id == self_id)
- .is_some();
-
- Stack::new()
- .with_child(
- TerminalElement::new(
- cx.handle(),
- terminal_handle,
- focused,
- self.should_show_cursor(focused, cx),
- )
- .contained()
- .boxed(),
- )
- .with_child(ChildView::new(&self.context_menu, cx).boxed())
- .boxed()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_new_content = false;
- self.terminal.read(cx).focus_in();
- self.blink_cursors(self.blink_epoch, cx);
- cx.notify();
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.terminal.update(cx, |terminal, _| {
- terminal.focus_out();
- });
- cx.notify();
- }
-
- fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
- self.clear_bel(cx);
- self.pause_cursor_blinking(cx);
-
- self.terminal.update(cx, |term, cx| {
- term.try_keystroke(
- &event.keystroke,
- cx.global::<Settings>()
- .terminal_overrides
- .option_as_meta
- .unwrap_or(false),
- )
- })
- }
-
- //IME stuff
- fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
- if self
- .terminal
- .read(cx)
- .last_content
- .mode
- .contains(TermMode::ALT_SCREEN)
- {
- None
- } else {
- Some(0..0)
- }
- }
-
- fn replace_text_in_range(
- &mut self,
- _: Option<std::ops::Range<usize>>,
- text: &str,
- cx: &mut ViewContext<Self>,
- ) {
- self.terminal.update(cx, |terminal, _| {
- terminal.input(text.into());
- });
- }
-
- fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
- let mut context = Self::default_keymap_context();
- if self.modal {
- context.set.insert("ModalTerminal".into());
- }
- let mode = self.terminal.read(cx).last_content.mode;
- context.map.insert(
- "screen".to_string(),
- (if mode.contains(TermMode::ALT_SCREEN) {
- "alt"
- } else {
- "normal"
- })
- .to_string(),
- );
-
- if mode.contains(TermMode::APP_CURSOR) {
- context.set.insert("DECCKM".to_string());
- }
- if mode.contains(TermMode::APP_KEYPAD) {
- context.set.insert("DECPAM".to_string());
- }
- //Note the ! here
- if !mode.contains(TermMode::APP_KEYPAD) {
- context.set.insert("DECPNM".to_string());
- }
- if mode.contains(TermMode::SHOW_CURSOR) {
- context.set.insert("DECTCEM".to_string());
- }
- if mode.contains(TermMode::LINE_WRAP) {
- context.set.insert("DECAWM".to_string());
- }
- if mode.contains(TermMode::ORIGIN) {
- context.set.insert("DECOM".to_string());
- }
- if mode.contains(TermMode::INSERT) {
- context.set.insert("IRM".to_string());
- }
- //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
- if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
- context.set.insert("LNM".to_string());
- }
- if mode.contains(TermMode::FOCUS_IN_OUT) {
- context.set.insert("report_focus".to_string());
- }
- if mode.contains(TermMode::ALTERNATE_SCROLL) {
- context.set.insert("alternate_scroll".to_string());
- }
- if mode.contains(TermMode::BRACKETED_PASTE) {
- context.set.insert("bracketed_paste".to_string());
- }
- if mode.intersects(TermMode::MOUSE_MODE) {
- context.set.insert("any_mouse_reporting".to_string());
- }
- {
- let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
- "click"
- } else if mode.contains(TermMode::MOUSE_DRAG) {
- "drag"
- } else if mode.contains(TermMode::MOUSE_MOTION) {
- "motion"
- } else {
- "off"
- };
- context
- .map
- .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
- }
- {
- let format = if mode.contains(TermMode::SGR_MOUSE) {
- "sgr"
- } else if mode.contains(TermMode::UTF8_MOUSE) {
- "utf8"
- } else {
- "normal"
- };
- context
- .map
- .insert("mouse_format".to_string(), format.to_string());
- }
- context
- }
-}
@@ -1,143 +0,0 @@
-use std::{path::Path, time::Duration};
-
-use alacritty_terminal::{
- index::{Column, Line, Point},
- term::cell::Cell,
-};
-use gpui::{ModelHandle, TestAppContext, ViewHandle};
-
-use project::{Entry, Project, ProjectPath, Worktree};
-use rand::{rngs::ThreadRng, Rng};
-use workspace::{AppState, Workspace};
-
-use crate::{IndexedCell, TerminalContent, TerminalSize};
-
-pub struct TerminalTestContext<'a> {
- pub cx: &'a mut TestAppContext,
-}
-
-impl<'a> TerminalTestContext<'a> {
- pub fn new(cx: &'a mut TestAppContext) -> Self {
- cx.set_condition_duration(Some(Duration::from_secs(5)));
-
- TerminalTestContext { cx }
- }
-
- ///Creates a worktree with 1 file: /root.txt
- pub async fn blank_workspace(&mut self) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
- let params = self.cx.update(AppState::test);
-
- let project = Project::test(params.fs.clone(), [], self.cx).await;
- let (_, workspace) = self.cx.add_window(|cx| {
- Workspace::new(
- Default::default(),
- 0,
- project.clone(),
- |_, _| unimplemented!(),
- cx,
- )
- });
-
- (project, workspace)
- }
-
- ///Creates a worktree with 1 folder: /root{suffix}/
- pub async fn create_folder_wt(
- &mut self,
- project: ModelHandle<Project>,
- path: impl AsRef<Path>,
- ) -> (ModelHandle<Worktree>, Entry) {
- self.create_wt(project, true, path).await
- }
-
- ///Creates a worktree with 1 file: /root{suffix}.txt
- pub async fn create_file_wt(
- &mut self,
- project: ModelHandle<Project>,
- path: impl AsRef<Path>,
- ) -> (ModelHandle<Worktree>, Entry) {
- self.create_wt(project, false, path).await
- }
-
- async fn create_wt(
- &mut self,
- project: ModelHandle<Project>,
- is_dir: bool,
- path: impl AsRef<Path>,
- ) -> (ModelHandle<Worktree>, Entry) {
- let (wt, _) = project
- .update(self.cx, |project, cx| {
- project.find_or_create_local_worktree(path, true, cx)
- })
- .await
- .unwrap();
-
- let entry = self
- .cx
- .update(|cx| {
- wt.update(cx, |wt, cx| {
- wt.as_local()
- .unwrap()
- .create_entry(Path::new(""), is_dir, cx)
- })
- })
- .await
- .unwrap();
-
- (wt, entry)
- }
-
- pub fn insert_active_entry_for(
- &mut self,
- wt: ModelHandle<Worktree>,
- entry: Entry,
- project: ModelHandle<Project>,
- ) {
- self.cx.update(|cx| {
- let p = ProjectPath {
- worktree_id: wt.read(cx).id(),
- path: entry.path,
- };
- project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
- });
- }
-
- pub fn create_terminal_content(
- size: TerminalSize,
- rng: &mut ThreadRng,
- ) -> (TerminalContent, Vec<Vec<char>>) {
- let mut ic = Vec::new();
- let mut cells = Vec::new();
-
- for row in 0..((size.height() / size.line_height()) as usize) {
- let mut row_vec = Vec::new();
- for col in 0..((size.width() / size.cell_width()) as usize) {
- let cell_char = rng.gen();
- ic.push(IndexedCell {
- point: Point::new(Line(row as i32), Column(col)),
- cell: Cell {
- c: cell_char,
- ..Default::default()
- },
- });
- row_vec.push(cell_char)
- }
- cells.push(row_vec)
- }
-
- (
- TerminalContent {
- cells: ic,
- size,
- ..Default::default()
- },
- cells,
- )
- }
-}
-
-impl<'a> Drop for TerminalTestContext<'a> {
- fn drop(&mut self) {
- self.cx.set_condition_duration(None);
- }
-}
@@ -0,0 +1,44 @@
+[package]
+name = "terminal_view"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/terminal_view.rs"
+doctest = false
+
+[dependencies]
+context_menu = { path = "../context_menu" }
+editor = { path = "../editor" }
+language = { path = "../language" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+db = { path = "../db" }
+procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
+terminal = { path = "../terminal" }
+smallvec = { version = "1.6", features = ["union"] }
+smol = "1.2.5"
+mio-extras = "2.0.6"
+futures = "0.3"
+ordered-float = "2.1.1"
+itertools = "0.10"
+dirs = "4.0.0"
+shellexpand = "2.1.0"
+libc = "0.2"
+anyhow = "1"
+thiserror = "1.0"
+lazy_static = "1.4.0"
+serde = { version = "1.0", features = ["derive"] }
+
+
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"]}
+project = { path = "../project", features = ["test-support"]}
+workspace = { path = "../workspace", features = ["test-support"] }
+rand = "0.8.5"
@@ -1,11 +1,10 @@
use std::path::PathBuf;
use db::{define_connection, query, sqlez_macros::sql};
-
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection! {
- pub static ref TERMINAL_CONNECTION: TerminalDb<WorkspaceDb> =
+ pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
&[sql!(
CREATE TABLE terminals (
workspace_id INTEGER,
@@ -13,7 +12,7 @@ define_connection! {
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
+ ON DELETE CASCADE
) STRICT;
)];
}
@@ -43,10 +42,10 @@ impl TerminalDb {
}
query! {
- pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
- SELECT working_directory
- FROM terminals
+ pub async fn take_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+ DELETE FROM terminals
WHERE item_id = ? AND workspace_id = ?
+ RETURNING working_directory
}
}
}
@@ -1,9 +1,3 @@
-use alacritty_terminal::{
- ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
- grid::Dimensions,
- index::Point,
- term::{cell::Flags, TermMode},
-};
use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
@@ -22,17 +16,23 @@ use itertools::Itertools;
use language::CursorShape;
use ordered_float::OrderedFloat;
use settings::Settings;
+use terminal::{
+ alacritty_terminal::{
+ ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
+ grid::Dimensions,
+ index::Point,
+ term::{cell::Flags, TermMode},
+ },
+ mappings::colors::convert_color,
+ IndexedCell, Terminal, TerminalContent, TerminalSize,
+};
use theme::TerminalStyle;
use util::ResultExt;
use std::{fmt::Debug, ops::RangeInclusive};
use std::{mem, ops::Range};
-use crate::{
- mappings::colors::convert_color,
- terminal_view::{DeployContextMenu, TerminalView},
- IndexedCell, Terminal, TerminalContent, TerminalSize,
-};
+use crate::{DeployContextMenu, TerminalView};
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
@@ -299,7 +299,7 @@ impl TerminalElement {
///Convert the Alacritty cell styles to GPUI text styles and background color
fn cell_style(
indexed: &IndexedCell,
- fg: AnsiColor,
+ fg: terminal::alacritty_terminal::ansi::Color,
style: &TerminalStyle,
text_style: &TextStyle,
font_cache: &FontCache,
@@ -0,0 +1,1091 @@
+mod persistence;
+pub mod terminal_element;
+
+use std::{
+ ops::RangeInclusive,
+ path::{Path, PathBuf},
+ time::Duration,
+};
+
+use context_menu::{ContextMenu, ContextMenuItem};
+use dirs::home_dir;
+use gpui::{
+ actions,
+ elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
+ geometry::vector::Vector2F,
+ impl_actions, impl_internal_actions,
+ keymap::Keystroke,
+ AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
+ View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::{LocalWorktree, Project, ProjectPath};
+use serde::Deserialize;
+use settings::{Settings, TerminalBlink, WorkingDirectory};
+use smallvec::SmallVec;
+use smol::Timer;
+use terminal::{
+ alacritty_terminal::{
+ index::Point,
+ term::{search::RegexSearch, TermMode},
+ },
+ Event, Terminal,
+};
+use util::{truncate_and_trailoff, ResultExt};
+use workspace::{
+ item::{Item, ItemEvent},
+ notifications::NotifyResultExt,
+ pane, register_deserializable_item,
+ searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
+ Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
+
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
+
+///Event to transmit the scroll from the element to the view
+#[derive(Clone, Debug, PartialEq)]
+pub struct ScrollTerminal(pub i32);
+
+#[derive(Clone, PartialEq)]
+pub struct DeployContextMenu {
+ pub position: Vector2F,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct SendText(String);
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct SendKeystroke(String);
+
+actions!(
+ terminal,
+ [Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
+);
+
+impl_actions!(terminal, [SendText, SendKeystroke]);
+
+impl_internal_actions!(project_panel, [DeployContextMenu]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(TerminalView::deploy);
+
+ register_deserializable_item::<TerminalView>(cx);
+
+ //Useful terminal views
+ cx.add_action(TerminalView::send_text);
+ cx.add_action(TerminalView::send_keystroke);
+ cx.add_action(TerminalView::deploy_context_menu);
+ cx.add_action(TerminalView::copy);
+ cx.add_action(TerminalView::paste);
+ cx.add_action(TerminalView::clear);
+ cx.add_action(TerminalView::show_character_palette);
+}
+
+///A terminal view, maintains the PTY's file handles and communicates with the terminal
+pub struct TerminalView {
+ terminal: ModelHandle<Terminal>,
+ has_new_content: bool,
+ //Currently using iTerm bell, show bell emoji in tab until input is received
+ has_bell: bool,
+ context_menu: ViewHandle<ContextMenu>,
+ blink_state: bool,
+ blinking_on: bool,
+ blinking_paused: bool,
+ blink_epoch: usize,
+ workspace_id: WorkspaceId,
+}
+
+impl Entity for TerminalView {
+ type Event = Event;
+}
+
+impl TerminalView {
+ ///Create a new Terminal in the current working directory or the user's home directory
+ pub fn deploy(
+ workspace: &mut Workspace,
+ _: &workspace::NewTerminal,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let strategy = cx.global::<Settings>().terminal_strategy();
+
+ let working_directory = get_working_directory(workspace, cx, strategy);
+
+ let window_id = cx.window_id();
+ let terminal = workspace
+ .project()
+ .update(cx, |project, cx| {
+ project.create_terminal(working_directory, window_id, cx)
+ })
+ .notify_err(workspace, cx);
+
+ if let Some(terminal) = terminal {
+ let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+ workspace.add_item(Box::new(view), cx)
+ }
+ }
+
+ pub fn new(
+ terminal: ModelHandle<Terminal>,
+ workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
+ cx.subscribe(&terminal, |this, _, event, cx| match event {
+ Event::Wakeup => {
+ if !cx.is_self_focused() {
+ this.has_new_content = true;
+ cx.notify();
+ }
+ cx.emit(Event::Wakeup);
+ }
+ Event::Bell => {
+ this.has_bell = true;
+ cx.emit(Event::Wakeup);
+ }
+ Event::BlinkChanged => this.blinking_on = !this.blinking_on,
+ Event::TitleChanged => {
+ if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
+ let cwd = foreground_info.cwd.clone();
+
+ let item_id = cx.view_id();
+ let workspace_id = this.workspace_id;
+ cx.background()
+ .spawn(async move {
+ TERMINAL_DB
+ .save_working_directory(item_id, workspace_id, cwd)
+ .await
+ .log_err();
+ })
+ .detach();
+ }
+ }
+ _ => cx.emit(*event),
+ })
+ .detach();
+
+ Self {
+ terminal,
+ has_new_content: true,
+ has_bell: false,
+ context_menu: cx.add_view(ContextMenu::new),
+ blink_state: true,
+ blinking_on: false,
+ blinking_paused: false,
+ blink_epoch: 0,
+ workspace_id,
+ }
+ }
+
+ pub fn handle(&self) -> ModelHandle<Terminal> {
+ self.terminal.clone()
+ }
+
+ pub fn has_new_content(&self) -> bool {
+ self.has_new_content
+ }
+
+ pub fn has_bell(&self) -> bool {
+ self.has_bell
+ }
+
+ pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
+ self.has_bell = false;
+ cx.emit(Event::Wakeup);
+ }
+
+ pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+ let menu_entries = vec![
+ ContextMenuItem::item("Clear", Clear),
+ ContextMenuItem::item("Close", pane::CloseActiveItem),
+ ];
+
+ self.context_menu.update(cx, |menu, cx| {
+ menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
+ });
+
+ cx.notify();
+ }
+
+ fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+ if !self
+ .terminal
+ .read(cx)
+ .last_content
+ .mode
+ .contains(TermMode::ALT_SCREEN)
+ {
+ cx.show_character_palette();
+ } else {
+ self.terminal.update(cx, |term, cx| {
+ term.try_keystroke(
+ &Keystroke::parse("ctrl-cmd-space").unwrap(),
+ cx.global::<Settings>()
+ .terminal_overrides
+ .option_as_meta
+ .unwrap_or(false),
+ )
+ });
+ }
+ }
+
+ fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
+ self.terminal.update(cx, |term, _| term.clear());
+ cx.notify();
+ }
+
+ pub fn should_show_cursor(
+ &self,
+ focused: bool,
+ cx: &mut gpui::RenderContext<'_, Self>,
+ ) -> bool {
+ //Don't blink the cursor when not focused, blinking is disabled, or paused
+ if !focused
+ || !self.blinking_on
+ || self.blinking_paused
+ || self
+ .terminal
+ .read(cx)
+ .last_content
+ .mode
+ .contains(TermMode::ALT_SCREEN)
+ {
+ return true;
+ }
+
+ let setting = {
+ let settings = cx.global::<Settings>();
+ settings
+ .terminal_overrides
+ .blinking
+ .clone()
+ .unwrap_or(TerminalBlink::TerminalControlled)
+ };
+
+ match setting {
+ //If the user requested to never blink, don't blink it.
+ TerminalBlink::Off => true,
+ //If the terminal is controlling it, check terminal mode
+ TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
+ }
+ }
+
+ fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+ if epoch == self.blink_epoch && !self.blinking_paused {
+ self.blink_state = !self.blink_state;
+ cx.notify();
+
+ let epoch = self.next_blink_epoch();
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(CURSOR_BLINK_INTERVAL).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+ }
+ }
+ })
+ .detach();
+ }
+ }
+
+ pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
+ self.blink_state = true;
+ cx.notify();
+
+ let epoch = self.next_blink_epoch();
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(CURSOR_BLINK_INTERVAL).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+ }
+ }
+ })
+ .detach();
+ }
+
+ pub fn find_matches(
+ &mut self,
+ query: project::search::SearchQuery,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Vec<RangeInclusive<Point>>> {
+ let searcher = regex_search_for_query(query);
+
+ if let Some(searcher) = searcher {
+ self.terminal
+ .update(cx, |term, cx| term.find_matches(searcher, cx))
+ } else {
+ cx.background().spawn(async { Vec::new() })
+ }
+ }
+
+ pub fn terminal(&self) -> &ModelHandle<Terminal> {
+ &self.terminal
+ }
+
+ fn next_blink_epoch(&mut self) -> usize {
+ self.blink_epoch += 1;
+ self.blink_epoch
+ }
+
+ fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+ if epoch == self.blink_epoch {
+ self.blinking_paused = false;
+ self.blink_cursors(epoch, cx);
+ }
+ }
+
+ ///Attempt to paste the clipboard into the terminal
+ fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+ self.terminal.update(cx, |term, _| term.copy())
+ }
+
+ ///Attempt to paste the clipboard into the terminal
+ fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+ if let Some(item) = cx.read_from_clipboard() {
+ self.terminal
+ .update(cx, |terminal, _cx| terminal.paste(item.text()));
+ }
+ }
+
+ fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
+ self.clear_bel(cx);
+ self.terminal.update(cx, |term, _| {
+ term.input(text.0.to_string());
+ });
+ }
+
+ fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
+ if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
+ self.clear_bel(cx);
+ self.terminal.update(cx, |term, cx| {
+ term.try_keystroke(
+ &keystroke,
+ cx.global::<Settings>()
+ .terminal_overrides
+ .option_as_meta
+ .unwrap_or(false),
+ );
+ });
+ }
+ }
+}
+
+pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
+ let searcher = match query {
+ project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
+ project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query),
+ };
+ searcher.ok()
+}
+
+impl View for TerminalView {
+ fn ui_name() -> &'static str {
+ "Terminal"
+ }
+
+ fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+ let terminal_handle = self.terminal.clone().downgrade();
+
+ let self_id = cx.view_id();
+ let focused = cx
+ .focused_view_id(cx.window_id())
+ .filter(|view_id| *view_id == self_id)
+ .is_some();
+
+ Stack::new()
+ .with_child(
+ TerminalElement::new(
+ cx.handle(),
+ terminal_handle,
+ focused,
+ self.should_show_cursor(focused, cx),
+ )
+ .contained()
+ .boxed(),
+ )
+ .with_child(ChildView::new(&self.context_menu, cx).boxed())
+ .boxed()
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_new_content = false;
+ self.terminal.read(cx).focus_in();
+ self.blink_cursors(self.blink_epoch, cx);
+ cx.notify();
+ }
+
+ fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.terminal.update(cx, |terminal, _| {
+ terminal.focus_out();
+ });
+ cx.notify();
+ }
+
+ fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
+ self.clear_bel(cx);
+ self.pause_cursor_blinking(cx);
+
+ self.terminal.update(cx, |term, cx| {
+ term.try_keystroke(
+ &event.keystroke,
+ cx.global::<Settings>()
+ .terminal_overrides
+ .option_as_meta
+ .unwrap_or(false),
+ )
+ })
+ }
+
+ //IME stuff
+ fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
+ if self
+ .terminal
+ .read(cx)
+ .last_content
+ .mode
+ .contains(TermMode::ALT_SCREEN)
+ {
+ None
+ } else {
+ Some(0..0)
+ }
+ }
+
+ fn replace_text_in_range(
+ &mut self,
+ _: Option<std::ops::Range<usize>>,
+ text: &str,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.terminal.update(cx, |terminal, _| {
+ terminal.input(text.into());
+ });
+ }
+
+ fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
+ let mut context = Self::default_keymap_context();
+
+ let mode = self.terminal.read(cx).last_content.mode;
+ context.map.insert(
+ "screen".to_string(),
+ (if mode.contains(TermMode::ALT_SCREEN) {
+ "alt"
+ } else {
+ "normal"
+ })
+ .to_string(),
+ );
+
+ if mode.contains(TermMode::APP_CURSOR) {
+ context.set.insert("DECCKM".to_string());
+ }
+ if mode.contains(TermMode::APP_KEYPAD) {
+ context.set.insert("DECPAM".to_string());
+ }
+ //Note the ! here
+ if !mode.contains(TermMode::APP_KEYPAD) {
+ context.set.insert("DECPNM".to_string());
+ }
+ if mode.contains(TermMode::SHOW_CURSOR) {
+ context.set.insert("DECTCEM".to_string());
+ }
+ if mode.contains(TermMode::LINE_WRAP) {
+ context.set.insert("DECAWM".to_string());
+ }
+ if mode.contains(TermMode::ORIGIN) {
+ context.set.insert("DECOM".to_string());
+ }
+ if mode.contains(TermMode::INSERT) {
+ context.set.insert("IRM".to_string());
+ }
+ //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
+ if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
+ context.set.insert("LNM".to_string());
+ }
+ if mode.contains(TermMode::FOCUS_IN_OUT) {
+ context.set.insert("report_focus".to_string());
+ }
+ if mode.contains(TermMode::ALTERNATE_SCROLL) {
+ context.set.insert("alternate_scroll".to_string());
+ }
+ if mode.contains(TermMode::BRACKETED_PASTE) {
+ context.set.insert("bracketed_paste".to_string());
+ }
+ if mode.intersects(TermMode::MOUSE_MODE) {
+ context.set.insert("any_mouse_reporting".to_string());
+ }
+ {
+ let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
+ "click"
+ } else if mode.contains(TermMode::MOUSE_DRAG) {
+ "drag"
+ } else if mode.contains(TermMode::MOUSE_MOTION) {
+ "motion"
+ } else {
+ "off"
+ };
+ context
+ .map
+ .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
+ }
+ {
+ let format = if mode.contains(TermMode::SGR_MOUSE) {
+ "sgr"
+ } else if mode.contains(TermMode::UTF8_MOUSE) {
+ "utf8"
+ } else {
+ "normal"
+ };
+ context
+ .map
+ .insert("mouse_format".to_string(), format.to_string());
+ }
+ context
+ }
+}
+
+impl Item for TerminalView {
+ fn tab_content(
+ &self,
+ _detail: Option<usize>,
+ tab_theme: &theme::Tab,
+ cx: &gpui::AppContext,
+ ) -> ElementBox {
+ let title = self
+ .terminal()
+ .read(cx)
+ .foreground_process_info
+ .as_ref()
+ .map(|fpi| {
+ format!(
+ "{} — {}",
+ truncate_and_trailoff(
+ &fpi.cwd
+ .file_name()
+ .map(|name| name.to_string_lossy().to_string())
+ .unwrap_or_default(),
+ 25
+ ),
+ truncate_and_trailoff(
+ &{
+ format!(
+ "{}{}",
+ fpi.name,
+ if fpi.argv.len() >= 1 {
+ format!(" {}", (&fpi.argv[1..]).join(" "))
+ } else {
+ "".to_string()
+ }
+ )
+ },
+ 25
+ )
+ )
+ })
+ .unwrap_or_else(|| "Terminal".to_string());
+
+ Flex::row()
+ .with_child(
+ Label::new(title, tab_theme.label.clone())
+ .aligned()
+ .contained()
+ .boxed(),
+ )
+ .boxed()
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ _cx: &mut ViewContext<Self>,
+ ) -> Option<Self> {
+ //From what I can tell, there's no way to tell the current working
+ //Directory of the terminal from outside the shell. There might be
+ //solutions to this, but they are non-trivial and require more IPC
+
+ // Some(TerminalContainer::new(
+ // Err(anyhow::anyhow!("failed to instantiate terminal")),
+ // workspace_id,
+ // cx,
+ // ))
+
+ // TODO
+ None
+ }
+
+ fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
+ None
+ }
+
+ fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
+ SmallVec::new()
+ }
+
+ fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
+ false
+ }
+
+ fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
+
+ fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+ false
+ }
+
+ fn save(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ unreachable!("save should not have been called");
+ }
+
+ fn save_as(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _abs_path: std::path::PathBuf,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ unreachable!("save_as should not have been called");
+ }
+
+ fn reload(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ gpui::Task::ready(Ok(()))
+ }
+
+ fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
+ self.has_bell()
+ }
+
+ fn has_conflict(&self, _cx: &AppContext) -> bool {
+ false
+ }
+
+ fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(handle.clone()))
+ }
+
+ fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
+ match event {
+ Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs],
+ Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab],
+ Event::CloseTerminal => vec![ItemEvent::CloseItem],
+ _ => vec![],
+ }
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft { flex: None }
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+ Some(vec![Text::new(
+ self.terminal().read(cx).breadcrumb_text.to_string(),
+ theme.breadcrumbs.text.clone(),
+ )
+ .boxed()])
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("Terminal")
+ }
+
+ fn deserialize(
+ project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ let window_id = cx.window_id();
+ cx.spawn(|pane, mut cx| async move {
+ let cwd = TERMINAL_DB
+ .take_working_directory(item_id, workspace_id)
+ .await
+ .log_err()
+ .flatten();
+
+ cx.update(|cx| {
+ let terminal = project.update(cx, |project, cx| {
+ project.create_terminal(cwd, window_id, cx)
+ })?;
+
+ Ok(cx.add_view(pane, |cx| TerminalView::new(terminal, workspace_id, cx)))
+ })
+ })
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ cx.background()
+ .spawn(TERMINAL_DB.update_workspace_id(
+ workspace.database_id(),
+ self.workspace_id,
+ cx.view_id(),
+ ))
+ .detach();
+ self.workspace_id = workspace.database_id();
+ }
+}
+
+impl SearchableItem for TerminalView {
+ type Match = RangeInclusive<Point>;
+
+ fn supported_options() -> SearchOptions {
+ SearchOptions {
+ case: false,
+ word: false,
+ regex: false,
+ }
+ }
+
+ /// Convert events raised by this item into search-relevant events (if applicable)
+ fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
+ match event {
+ Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
+ Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
+ _ => None,
+ }
+ }
+
+ /// Clear stored matches
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+ self.terminal().update(cx, |term, _| term.matches.clear())
+ }
+
+ /// Store matches returned from find_matches somewhere for rendering
+ fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+ self.terminal().update(cx, |term, _| term.matches = matches)
+ }
+
+ /// Return the selection content to pre-load into this search
+ fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+ self.terminal()
+ .read(cx)
+ .last_content
+ .selection_text
+ .clone()
+ .unwrap_or_default()
+ }
+
+ /// Focus match at given index into the Vec of matches
+ fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+ self.terminal()
+ .update(cx, |term, _| term.activate_match(index));
+ cx.notify();
+ }
+
+ /// Get all of the matches for this query, should be done on the background
+ fn find_matches(
+ &mut self,
+ query: project::search::SearchQuery,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Vec<Self::Match>> {
+ if let Some(searcher) = regex_search_for_query(query) {
+ self.terminal()
+ .update(cx, |term, cx| term.find_matches(searcher, cx))
+ } else {
+ Task::ready(vec![])
+ }
+ }
+
+ /// Reports back to the search toolbar what the active match should be (the selection)
+ fn active_match_index(
+ &mut self,
+ matches: Vec<Self::Match>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<usize> {
+ // Selection head might have a value if there's a selection that isn't
+ // associated with a match. Therefore, if there are no matches, we should
+ // report None, no matter the state of the terminal
+ let res = if matches.len() > 0 {
+ if let Some(selection_head) = self.terminal().read(cx).selection_head {
+ // If selection head is contained in a match. Return that match
+ if let Some(ix) = matches
+ .iter()
+ .enumerate()
+ .find(|(_, search_match)| {
+ search_match.contains(&selection_head)
+ || search_match.start() > &selection_head
+ })
+ .map(|(ix, _)| ix)
+ {
+ Some(ix)
+ } else {
+ // If no selection after selection head, return the last match
+ Some(matches.len().saturating_sub(1))
+ }
+ } else {
+ // Matches found but no active selection, return the first last one (closest to cursor)
+ Some(matches.len().saturating_sub(1))
+ }
+ } else {
+ None
+ };
+
+ res
+ }
+}
+
+///Get's the working directory for the given workspace, respecting the user's settings.
+pub fn get_working_directory(
+ workspace: &Workspace,
+ cx: &AppContext,
+ strategy: WorkingDirectory,
+) -> Option<PathBuf> {
+ let res = match strategy {
+ WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
+ .or_else(|| first_project_directory(workspace, cx)),
+ WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
+ WorkingDirectory::AlwaysHome => None,
+ WorkingDirectory::Always { directory } => {
+ shellexpand::full(&directory) //TODO handle this better
+ .ok()
+ .map(|dir| Path::new(&dir.to_string()).to_path_buf())
+ .filter(|dir| dir.is_dir())
+ }
+ };
+ res.or_else(home_dir)
+}
+
+///Get's the first project's home directory, or the home directory
+fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+ workspace
+ .worktrees(cx)
+ .next()
+ .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+ .and_then(get_path_from_wt)
+}
+
+///Gets the intuitively correct working directory from the given workspace
+///If there is an active entry for this project, returns that entry's worktree root.
+///If there's no active entry but there is a worktree, returns that worktrees root.
+///If either of these roots are files, or if there are any other query failures,
+/// returns the user's home directory
+fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+ let project = workspace.project().read(cx);
+
+ project
+ .active_entry()
+ .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+ .or_else(|| workspace.worktrees(cx).next())
+ .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+ .and_then(get_path_from_wt)
+}
+
+fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
+ wt.root_entry()
+ .filter(|re| re.is_dir())
+ .map(|_| wt.abs_path().to_path_buf())
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use gpui::TestAppContext;
+ use project::{Entry, Project, ProjectPath, Worktree};
+ use workspace::AppState;
+
+ use std::path::Path;
+
+ ///Working directory calculation tests
+
+ ///No Worktrees in project -> home_dir()
+ #[gpui::test]
+ async fn no_worktree(cx: &mut TestAppContext) {
+ //Setup variables
+ let (project, workspace) = blank_workspace(cx).await;
+ //Test
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ let active_entry = project.read(cx).active_entry();
+
+ //Make sure enviroment is as expeted
+ assert!(active_entry.is_none());
+ assert!(workspace.worktrees(cx).next().is_none());
+
+ let res = current_project_directory(workspace, cx);
+ assert_eq!(res, None);
+ let res = first_project_directory(workspace, cx);
+ assert_eq!(res, None);
+ });
+ }
+
+ ///No active entry, but a worktree, worktree is a file -> home_dir()
+ #[gpui::test]
+ async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
+ //Setup variables
+
+ let (project, workspace) = blank_workspace(cx).await;
+ create_file_wt(project.clone(), "/root.txt", cx).await;
+
+ cx.read(|cx| {
+ let workspace = workspace.read(cx);
+ let active_entry = project.read(cx).active_entry();
+
+ //Make sure enviroment is as expeted
+ assert!(active_entry.is_none());
+ assert!(workspace.worktrees(cx).next().is_some());
+
+ let res = current_project_directory(workspace, cx);
+ assert_eq!(res, None);
+ let res = first_project_directory(workspace, cx);
+ assert_eq!(res, None);
+ });
+ }
+
+ //No active entry, but a worktree, worktree is a folder -> worktree_folder
+ #[gpui::test]
+ async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+ //Setup variables
+ let (project, workspace) = blank_workspace(cx).await;
+ let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
+
+ //Test
+ cx.update(|cx| {
+ let workspace = workspace.read(cx);
+ let active_entry = project.read(cx).active_entry();
+
+ assert!(active_entry.is_none());
+ assert!(workspace.worktrees(cx).next().is_some());
+
+ let res = current_project_directory(workspace, cx);
+ assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+ let res = first_project_directory(workspace, cx);
+ assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+ });
+ }
+
+ //Active entry with a work tree, worktree is a file -> home_dir()
+ #[gpui::test]
+ async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
+ //Setup variables
+
+ let (project, workspace) = blank_workspace(cx).await;
+ let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
+ let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
+ insert_active_entry_for(wt2, entry2, project.clone(), cx);
+
+ //Test
+ cx.update(|cx| {
+ let workspace = workspace.read(cx);
+ let active_entry = project.read(cx).active_entry();
+
+ assert!(active_entry.is_some());
+
+ let res = current_project_directory(workspace, cx);
+ assert_eq!(res, None);
+ let res = first_project_directory(workspace, cx);
+ assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+ });
+ }
+
+ //Active entry, with a worktree, worktree is a folder -> worktree_folder
+ #[gpui::test]
+ async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+ //Setup variables
+ let (project, workspace) = blank_workspace(cx).await;
+ let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
+ let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
+ insert_active_entry_for(wt2, entry2, project.clone(), cx);
+
+ //Test
+ cx.update(|cx| {
+ let workspace = workspace.read(cx);
+ let active_entry = project.read(cx).active_entry();
+
+ assert!(active_entry.is_some());
+
+ let res = current_project_directory(workspace, cx);
+ assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
+ let res = first_project_directory(workspace, cx);
+ assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+ });
+ }
+
+ ///Creates a worktree with 1 file: /root.txt
+ pub async fn blank_workspace(
+ cx: &mut TestAppContext,
+ ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
+ let params = cx.update(AppState::test);
+
+ let project = Project::test(params.fs.clone(), [], cx).await;
+ let (_, workspace) = cx.add_window(|cx| {
+ Workspace::new(
+ Default::default(),
+ 0,
+ project.clone(),
+ |_, _| unimplemented!(),
+ cx,
+ )
+ });
+
+ (project, workspace)
+ }
+
+ ///Creates a worktree with 1 folder: /root{suffix}/
+ async fn create_folder_wt(
+ project: ModelHandle<Project>,
+ path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) -> (ModelHandle<Worktree>, Entry) {
+ create_wt(project, true, path, cx).await
+ }
+
+ ///Creates a worktree with 1 file: /root{suffix}.txt
+ async fn create_file_wt(
+ project: ModelHandle<Project>,
+ path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) -> (ModelHandle<Worktree>, Entry) {
+ create_wt(project, false, path, cx).await
+ }
+
+ async fn create_wt(
+ project: ModelHandle<Project>,
+ is_dir: bool,
+ path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) -> (ModelHandle<Worktree>, Entry) {
+ let (wt, _) = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree(path, true, cx)
+ })
+ .await
+ .unwrap();
+
+ let entry = cx
+ .update(|cx| {
+ wt.update(cx, |wt, cx| {
+ wt.as_local()
+ .unwrap()
+ .create_entry(Path::new(""), is_dir, cx)
+ })
+ })
+ .await
+ .unwrap();
+
+ (wt, entry)
+ }
+
+ pub fn insert_active_entry_for(
+ wt: ModelHandle<Worktree>,
+ entry: Entry,
+ project: ModelHandle<Project>,
+ cx: &mut TestAppContext,
+ ) {
+ cx.update(|cx| {
+ let p = ProjectPath {
+ worktree_id: wt.read(cx).id(),
+ path: entry.path,
+ };
+ project.update(cx, |project, cx| project.set_active_path(Some(p), 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
}
@@ -126,18 +126,21 @@ impl DockPosition {
}
}
-pub type DefaultItemFactory =
- fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
+pub type DockDefaultItemFactory =
+ fn(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Box<dyn ItemHandle>>;
pub struct Dock {
position: DockPosition,
panel_sizes: HashMap<DockAnchor, f32>,
pane: ViewHandle<Pane>,
- default_item_factory: DefaultItemFactory,
+ default_item_factory: DockDefaultItemFactory,
}
impl Dock {
- pub fn new(default_item_factory: DefaultItemFactory, cx: &mut ViewContext<Workspace>) -> Self {
+ pub fn new(
+ default_item_factory: DockDefaultItemFactory,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Self {
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
@@ -192,9 +195,11 @@ impl Dock {
// Ensure that the pane has at least one item or construct a default item to put in it
let pane = workspace.dock.pane.clone();
if pane.read(cx).items().next().is_none() {
- let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
- // Adding the item focuses the pane by default
- Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
+ if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
+ Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
+ } else {
+ workspace.dock.position = workspace.dock.position.hide();
+ }
} else {
cx.focus(pane);
}
@@ -477,8 +482,8 @@ mod tests {
pub fn default_item_factory(
_workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
- ) -> Box<dyn ItemHandle> {
- Box::new(cx.add_view(|_| TestItem::new()))
+ ) -> Option<Box<dyn ItemHandle>> {
+ Some(Box::new(cx.add_view(|_| TestItem::new())))
}
#[gpui::test]
@@ -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 {
@@ -161,8 +161,8 @@ pub mod simple_message_notification {
pub struct MessageNotification {
message: String,
- click_action: Box<dyn Action>,
- click_message: String,
+ click_action: Option<Box<dyn Action>>,
+ click_message: Option<String>,
}
pub enum MessageNotificationEvent {
@@ -174,6 +174,14 @@ pub mod simple_message_notification {
}
impl MessageNotification {
+ pub fn new_messsage<S: AsRef<str>>(message: S) -> MessageNotification {
+ Self {
+ message: message.as_ref().to_string(),
+ click_action: None,
+ click_message: None,
+ }
+ }
+
pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
message: S1,
click_action: A,
@@ -181,8 +189,8 @@ pub mod simple_message_notification {
) -> Self {
Self {
message: message.as_ref().to_string(),
- click_action: Box::new(click_action) as Box<dyn Action>,
- click_message: click_message.as_ref().to_string(),
+ click_action: Some(Box::new(click_action) as Box<dyn Action>),
+ click_message: Some(click_message.as_ref().to_string()),
}
}
@@ -198,12 +206,15 @@ pub mod simple_message_notification {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = cx.global::<Settings>().theme.clone();
- let theme = &theme.update_notification;
+ let theme = &theme.simple_message_notification;
enum MessageNotificationTag {}
- let click_action = self.click_action.boxed_clone();
- let click_message = self.click_message.clone();
+ let click_action = self
+ .click_action
+ .as_ref()
+ .map(|action| action.boxed_clone());
+ let click_message = self.click_message.as_ref().map(|message| message.clone());
let message = self.message.clone();
MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
@@ -251,20 +262,28 @@ pub mod simple_message_notification {
)
.boxed(),
)
- .with_child({
+ .with_children({
let style = theme.action_message.style_for(state, false);
-
- Text::new(click_message, style.text.clone())
- .contained()
- .with_style(style.container)
- .boxed()
+ if let Some(click_message) = click_message {
+ Some(
+ Text::new(click_message, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed(),
+ )
+ } else {
+ None
+ }
+ .into_iter()
})
.contained()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_any_action(click_action.boxed_clone())
+ if let Some(click_action) = click_action.as_ref() {
+ cx.dispatch_any_action(click_action.boxed_clone())
+ }
})
.boxed()
}
@@ -278,3 +297,38 @@ pub mod simple_message_notification {
}
}
}
+
+pub trait NotifyResultExt {
+ type Ok;
+
+ fn notify_err(
+ self,
+ workspace: &mut Workspace,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Self::Ok>;
+}
+
+impl<T, E> NotifyResultExt for Result<T, E>
+where
+ E: std::fmt::Debug,
+{
+ type Ok = T;
+
+ fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
+ match self {
+ Ok(value) => Some(value),
+ Err(err) => {
+ workspace.show_notification(0, cx, |cx| {
+ cx.add_view(|_cx| {
+ simple_message_notification::MessageNotification::new_messsage(format!(
+ "Error: {:?}",
+ err,
+ ))
+ })
+ });
+
+ None
+ }
+ }
+ }
+}
@@ -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::{DefaultItemFactory, Dock, ToggleDockButton};
+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::*;
@@ -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<_>) })
})
@@ -376,7 +382,7 @@ pub struct AppState {
pub fs: Arc<dyn fs::Fs>,
pub build_window_options: fn() -> WindowOptions<'static>,
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
- pub default_item_factory: DefaultItemFactory,
+ pub dock_default_item_factory: DockDefaultItemFactory,
}
impl AppState {
@@ -402,7 +408,7 @@ impl AppState {
user_store,
initialize_workspace: |_, _, _| {},
build_window_options: Default::default,
- default_item_factory: |_, _| unimplemented!(),
+ dock_default_item_factory: |_, _| unimplemented!(),
})
}
}
@@ -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,16 +494,37 @@ 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>,
workspace_id: WorkspaceId,
project: ModelHandle<Project>,
- dock_default_factory: DefaultItemFactory,
+ dock_default_factory: DockDefaultItemFactory,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
@@ -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));
@@ -704,7 +728,7 @@ impl Workspace {
serialized_workspace,
workspace_id,
project_handle,
- app_state.default_item_factory,
+ app_state.dock_default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, 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,23 +2136,6 @@ 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();
@@ -2126,7 +2148,7 @@ impl Workspace {
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))
{
@@ -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>;
}
@@ -2719,8 +2759,8 @@ mod tests {
pub fn default_item_factory(
_workspace: &mut Workspace,
_cx: &mut ViewContext<Workspace>,
- ) -> Box<dyn ItemHandle> {
- unimplemented!();
+ ) -> Option<Box<dyn ItemHandle>> {
+ unimplemented!()
}
#[gpui::test]
@@ -48,7 +48,7 @@ rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
-terminal = { path = "../terminal" }
+terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
theme_testbench = { path = "../theme_testbench" }
@@ -32,13 +32,15 @@ use settings::{
use smol::process::Command;
use std::fs::OpenOptions;
use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration};
-use terminal::terminal_container_view::{get_working_directory, TerminalContainer};
+use terminal_view::{get_working_directory, TerminalView};
use fs::RealFs;
use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::{self, item::ItemHandle, AppState, NewFile, OpenPaths, Workspace};
+use workspace::{
+ self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
+};
use zed::{self, build_window_options, initialize_workspace, languages, menus};
fn main() {
@@ -119,7 +121,7 @@ fn main() {
diagnostics::init(cx);
search::init(cx);
vim::init(cx);
- terminal::init(cx);
+ terminal_view::init(cx);
theme_testbench::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
@@ -150,7 +152,7 @@ fn main() {
fs,
build_window_options,
initialize_workspace,
- default_item_factory,
+ dock_default_item_factory,
});
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
@@ -591,10 +593,10 @@ async fn handle_cli_connection(
}
}
-pub fn default_item_factory(
+pub fn dock_default_item_factory(
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
-) -> Box<dyn ItemHandle> {
+) -> Option<Box<dyn ItemHandle>> {
let strategy = cx
.global::<Settings>()
.terminal_overrides
@@ -604,8 +606,15 @@ pub fn default_item_factory(
let working_directory = get_working_directory(workspace, cx, strategy);
- let terminal_handle = cx.add_view(|cx| {
- TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
- });
- Box::new(terminal_handle)
+ let window_id = cx.window_id();
+ let terminal = workspace
+ .project()
+ .update(cx, |project, cx| {
+ project.create_terminal(working_directory, window_id, cx)
+ })
+ .notify_err(workspace, cx)?;
+
+ let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+
+ Some(Box::new(terminal_view))
}
@@ -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
@@ -0,0 +1,50 @@
+#!/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_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,
@@ -7,11 +7,11 @@ export default function simpleMessageNotification(colorScheme: ColorScheme): Obj
let layer = colorScheme.middle;
return {
message: {
- ...text(layer, "sans", { size: "md" }),
+ ...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
actionMessage: {
- ...text(layer, "sans", { size: "md" }),
+ ...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, top: 6, bottom: 6 },
hover: {
color: foreground(layer, "hovered"),