Cargo.lock 🔗
@@ -8512,6 +8512,8 @@ dependencies = [
"anyhow",
"collections",
"futures 0.3.30",
+ "gpui",
+ "parking_lot",
"prost",
"prost-build",
"serde",
Max Brunsfeld , Mikayla , and Conrad created
This is a refactor to prepare for adding LSP support in SSH remote
projects.
Release Notes:
- N/A
---------
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Cargo.lock | 2
crates/assistant/src/assistant.rs | 2
crates/assistant/src/context_store.rs | 3
crates/channel/src/channel.rs | 4
crates/channel/src/channel_buffer.rs | 4
crates/channel/src/channel_chat.rs | 3
crates/client/src/client.rs | 307 +----
crates/collab/src/tests/test_server.rs | 2
crates/project/src/buffer_store.rs | 2
crates/project/src/lsp_store.rs | 963 ++++++-----------
crates/project/src/project.rs | 28
crates/project/src/worktree_store.rs | 21
crates/proto/Cargo.toml | 2
crates/proto/proto/zed.proto | 1
crates/proto/src/proto.rs | 55
crates/proto/src/proto_client.rs | 277 +++++
crates/remote/src/ssh_session.rs | 133 -
crates/remote_server/src/headless_project.rs | 65
crates/remote_server/src/remote_editing_tests.rs | 2
crates/rpc/src/peer.rs | 13
20 files changed, 827 insertions(+), 1,062 deletions(-)
@@ -8512,6 +8512,8 @@ dependencies = [
"anyhow",
"collections",
"futures 0.3.30",
+ "gpui",
+ "parking_lot",
"prost",
"prost-build",
"serde",
@@ -209,7 +209,7 @@ pub fn init(
})
.detach();
- context_store::init(&client);
+ context_store::init(&client.clone().into());
prompt_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
@@ -2,6 +2,7 @@ use crate::{
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
SavedContext, SavedContextMetadata,
};
+use ::proto::AnyProtoClient;
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
@@ -25,7 +26,7 @@ use std::{
};
use util::{ResultExt, TryFutureExt};
-pub fn init(client: &Arc<Client>) {
+pub fn init(client: &AnyProtoClient) {
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
client.add_model_request_handler(ContextStore::handle_open_context);
client.add_model_request_handler(ContextStore::handle_create_context);
@@ -18,6 +18,6 @@ mod channel_store_tests;
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
- channel_buffer::init(client);
- channel_chat::init(client);
+ channel_buffer::init(&client.clone().into());
+ channel_chat::init(&client.clone().into());
}
@@ -5,7 +5,7 @@ use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use language::proto::serialize_version;
use rpc::{
- proto::{self, PeerId},
+ proto::{self, AnyProtoClient, PeerId},
TypedEnvelope,
};
use std::{sync::Arc, time::Duration};
@@ -14,7 +14,7 @@ use util::ResultExt;
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
-pub(crate) fn init(client: &Arc<Client>) {
+pub(crate) fn init(client: &AnyProtoClient) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
}
@@ -11,6 +11,7 @@ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use rand::prelude::*;
+use rpc::proto::AnyProtoClient;
use std::{
ops::{ControlFlow, Range},
sync::Arc,
@@ -95,7 +96,7 @@ pub enum ChannelChatEvent {
}
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
-pub fn init(client: &Arc<Client>) {
+pub fn init(client: &AnyProtoClient) {
client.add_model_message_handler(ChannelChat::handle_message_sent);
client.add_model_message_handler(ChannelChat::handle_message_removed);
client.add_model_message_handler(ChannelChat::handle_message_updated);
@@ -14,22 +14,18 @@ use async_tungstenite::tungstenite::{
};
use chrono::{DateTime, Utc};
use clock::SystemClock;
-use collections::HashMap;
use futures::{
- channel::oneshot,
- future::{BoxFuture, LocalBoxFuture},
- AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
-};
-use gpui::{
- actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
+ channel::oneshot, future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
+ TryFutureExt as _, TryStreamExt,
};
+use gpui::{actions, AppContext, AsyncAppContext, Global, Model, Task, WeakModel};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use parking_lot::RwLock;
use postage::watch;
-use proto::ProtoClient;
+use proto::{AnyProtoClient, EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet};
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
-use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
+use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -208,6 +204,7 @@ pub struct Client {
telemetry: Arc<Telemetry>,
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
state: RwLock<ClientState>,
+ handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
#[allow(clippy::type_complexity)]
#[cfg(any(test, feature = "test-support"))]
@@ -304,30 +301,7 @@ impl Status {
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
- entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
- entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
- models_by_message_type: HashMap<TypeId, AnyWeakModel>,
- entity_types_by_message_type: HashMap<TypeId, TypeId>,
- #[allow(clippy::type_complexity)]
- message_handlers: HashMap<
- TypeId,
- Arc<
- dyn Send
- + Sync
- + Fn(
- AnyModel,
- Box<dyn AnyTypedEnvelope>,
- &Arc<Client>,
- AsyncAppContext,
- ) -> LocalBoxFuture<'static, Result<()>>,
- >,
- >,
-}
-
-enum WeakSubscriber {
- Entity { handle: AnyWeakModel },
- Pending(Vec<Box<dyn AnyTypedEnvelope>>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -379,12 +353,7 @@ impl Default for ClientState {
Self {
credentials: None,
status: watch::channel_with(Status::SignedOut),
- entity_id_extractors: Default::default(),
_reconnect_task: None,
- models_by_message_type: Default::default(),
- entities_by_type_and_remote_id: Default::default(),
- entity_types_by_message_type: Default::default(),
- message_handlers: Default::default(),
}
}
}
@@ -405,13 +374,13 @@ impl Drop for Subscription {
match self {
Subscription::Entity { client, id } => {
if let Some(client) = client.upgrade() {
- let mut state = client.state.write();
+ let mut state = client.handler_set.lock();
let _ = state.entities_by_type_and_remote_id.remove(id);
}
}
Subscription::Message { client, id } => {
if let Some(client) = client.upgrade() {
- let mut state = client.state.write();
+ let mut state = client.handler_set.lock();
let _ = state.entity_types_by_message_type.remove(id);
let _ = state.message_handlers.remove(id);
}
@@ -430,21 +399,21 @@ pub struct PendingEntitySubscription<T: 'static> {
impl<T: 'static> PendingEntitySubscription<T> {
pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
self.consumed = true;
- let mut state = self.client.state.write();
+ let mut handlers = self.client.handler_set.lock();
let id = (TypeId::of::<T>(), self.remote_id);
- let Some(WeakSubscriber::Pending(messages)) =
- state.entities_by_type_and_remote_id.remove(&id)
+ let Some(EntityMessageSubscriber::Pending(messages)) =
+ handlers.entities_by_type_and_remote_id.remove(&id)
else {
unreachable!()
};
- state.entities_by_type_and_remote_id.insert(
+ handlers.entities_by_type_and_remote_id.insert(
id,
- WeakSubscriber::Entity {
+ EntityMessageSubscriber::Entity {
handle: model.downgrade().into(),
},
);
- drop(state);
+ drop(handlers);
for message in messages {
let client_id = self.client.id();
let type_name = message.payload_type_name();
@@ -467,8 +436,8 @@ impl<T: 'static> PendingEntitySubscription<T> {
impl<T: 'static> Drop for PendingEntitySubscription<T> {
fn drop(&mut self) {
if !self.consumed {
- let mut state = self.client.state.write();
- if let Some(WeakSubscriber::Pending(messages)) = state
+ let mut state = self.client.handler_set.lock();
+ if let Some(EntityMessageSubscriber::Pending(messages)) = state
.entities_by_type_and_remote_id
.remove(&(TypeId::of::<T>(), self.remote_id))
{
@@ -549,6 +518,7 @@ impl Client {
http,
credentials_provider,
state: Default::default(),
+ handler_set: Default::default(),
#[cfg(any(test, feature = "test-support"))]
authenticate: Default::default(),
@@ -592,10 +562,7 @@ impl Client {
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.handler_set.lock().clear();
self.peer.teardown();
}
@@ -708,14 +675,14 @@ impl Client {
{
let id = (TypeId::of::<T>(), remote_id);
- let mut state = self.state.write();
+ let mut state = self.handler_set.lock();
if state.entities_by_type_and_remote_id.contains_key(&id) {
return Err(anyhow!("already subscribed to entity"));
}
state
.entities_by_type_and_remote_id
- .insert(id, WeakSubscriber::Pending(Default::default()));
+ .insert(id, EntityMessageSubscriber::Pending(Default::default()));
Ok(PendingEntitySubscription {
client: self.clone(),
@@ -752,13 +719,13 @@ impl Client {
E: 'static,
H: 'static
+ Sync
- + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F
+ + Fn(Model<E>, TypedEnvelope<M>, AnyProtoClient, AsyncAppContext) -> F
+ Send
+ Sync,
F: 'static + Future<Output = Result<()>>,
{
let message_type_id = TypeId::of::<M>();
- let mut state = self.state.write();
+ let mut state = self.handler_set.lock();
state
.models_by_message_type
.insert(message_type_id, entity.into());
@@ -803,85 +770,18 @@ impl Client {
})
}
- pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
- where
- M: EntityMessage,
- E: 'static,
- H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
- F: 'static + Future<Output = Result<()>>,
- {
- self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, _, cx| {
- handler(subscriber.downcast::<E>().unwrap(), message, cx)
- })
- }
-
- fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
- where
- M: EntityMessage,
- E: 'static,
- H: 'static + Fn(AnyModel, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F + Send + Sync,
- F: 'static + Future<Output = Result<()>>,
- {
- let model_type_id = TypeId::of::<E>();
- let message_type_id = TypeId::of::<M>();
-
- let mut state = self.state.write();
- state
- .entity_types_by_message_type
- .insert(message_type_id, model_type_id);
- state
- .entity_id_extractors
- .entry(message_type_id)
- .or_insert_with(|| {
- |envelope| {
- envelope
- .as_any()
- .downcast_ref::<TypedEnvelope<M>>()
- .unwrap()
- .payload
- .remote_entity_id()
- }
- });
- let prev_handler = state.message_handlers.insert(
- message_type_id,
- Arc::new(move |handle, envelope, client, cx| {
- let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
- handler(handle, *envelope, client.clone(), cx).boxed_local()
- }),
- );
- if prev_handler.is_some() {
- panic!("registered handler for the same message twice");
- }
- }
-
- pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
- where
- M: EntityMessage + RequestMessage,
- E: 'static,
- H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
- F: 'static + Future<Output = Result<M::Response>>,
- {
- self.add_entity_message_handler::<M, E, _, _>(move |entity, envelope, client, cx| {
- Self::respond_to_request::<M, _>(
- envelope.receipt(),
- handler(entity.downcast::<E>().unwrap(), envelope, cx),
- client,
- )
- })
- }
-
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
receipt: Receipt<T>,
response: F,
- client: Arc<Self>,
+ client: AnyProtoClient,
) -> Result<()> {
match response.await {
Ok(response) => {
- client.respond(receipt, response)?;
+ client.send_response(receipt.message_id, response)?;
Ok(())
}
Err(error) => {
- client.respond_with_error(receipt, error.to_proto())?;
+ client.send_response(receipt.message_id, error.to_proto())?;
Err(error)
}
}
@@ -1541,16 +1441,6 @@ impl Client {
self.peer.send(self.connection_id()?, message)
}
- pub fn send_dynamic(
- &self,
- envelope: proto::Envelope,
- message_type: &'static str,
- ) -> Result<()> {
- log::debug!("rpc send. client_id:{}, name:{}", self.id(), message_type);
- let connection_id = self.connection_id()?;
- self.peer.send_dynamic(connection_id, envelope)
- }
-
pub fn request<T: RequestMessage>(
&self,
request: T,
@@ -1632,115 +1522,56 @@ impl Client {
}
}
- fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
- log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
- self.peer.respond(receipt, response)
- }
-
- fn respond_with_error<T: RequestMessage>(
- &self,
- receipt: Receipt<T>,
- error: proto::Error,
- ) -> Result<()> {
- log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
- self.peer.respond_with_error(receipt, error)
- }
-
fn handle_message(
self: &Arc<Client>,
message: Box<dyn AnyTypedEnvelope>,
cx: &AsyncAppContext,
) {
- let mut state = self.state.write();
+ let sender_id = message.sender_id();
+ let request_id = message.message_id();
let type_name = message.payload_type_name();
- let payload_type_id = message.payload_type_id();
- let sender_id = message.original_sender_id();
-
- let mut subscriber = None;
-
- if let Some(handle) = state
- .models_by_message_type
- .get(&payload_type_id)
- .and_then(|handle| handle.upgrade())
- {
- subscriber = Some(handle);
- } else if let Some((extract_entity_id, entity_type_id)) =
- state.entity_id_extractors.get(&payload_type_id).zip(
- state
- .entity_types_by_message_type
- .get(&payload_type_id)
- .copied(),
- )
- {
- let entity_id = (extract_entity_id)(message.as_ref());
-
- match state
- .entities_by_type_and_remote_id
- .get_mut(&(entity_type_id, entity_id))
- {
- Some(WeakSubscriber::Pending(pending)) => {
- pending.push(message);
- return;
- }
- Some(weak_subscriber) => match weak_subscriber {
- WeakSubscriber::Entity { handle } => {
- subscriber = handle.upgrade();
- }
-
- WeakSubscriber::Pending(_) => {}
- },
- _ => {}
- }
- }
-
- let subscriber = if let Some(subscriber) = subscriber {
- subscriber
- } else {
- log::info!("unhandled message {}", type_name);
- self.peer.respond_with_unhandled_message(message).log_err();
- return;
- };
-
- let handler = state.message_handlers.get(&payload_type_id).cloned();
- // Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
- // It also ensures we don't hold the lock while yielding back to the executor, as
- // that might cause the executor thread driving this future to block indefinitely.
- drop(state);
-
- if let Some(handler) = handler {
- let future = handler(subscriber, message, self, cx.clone());
+ let original_sender_id = message.original_sender_id();
+
+ if let Some(future) = ProtoMessageHandlerSet::handle_message(
+ &self.handler_set,
+ message,
+ self.clone().into(),
+ cx.clone(),
+ ) {
let client_id = self.id();
log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
client_id,
- sender_id,
+ original_sender_id,
type_name
);
cx.spawn(move |_| async move {
- match future.await {
- Ok(()) => {
- log::debug!(
- "rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
- client_id,
- sender_id,
- type_name
- );
- }
- Err(error) => {
- log::error!(
- "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
- client_id,
- sender_id,
- type_name,
- error
- );
- }
+ match future.await {
+ Ok(()) => {
+ log::debug!(
+ "rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
+ client_id,
+ original_sender_id,
+ type_name
+ );
}
- })
- .detach();
+ Err(error) => {
+ log::error!(
+ "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
+ client_id,
+ original_sender_id,
+ type_name,
+ error
+ );
+ }
+ }
+ })
+ .detach();
} else {
log::info!("unhandled message {}", type_name);
- self.peer.respond_with_unhandled_message(message).log_err();
+ self.peer
+ .respond_with_unhandled_message(sender_id.into(), request_id, type_name)
+ .log_err();
}
}
@@ -1759,7 +1590,23 @@ impl ProtoClient for Client {
}
fn send(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
- self.send_dynamic(envelope, message_type)
+ log::debug!("rpc send. client_id:{}, name:{}", self.id(), message_type);
+ let connection_id = self.connection_id()?;
+ self.peer.send_dynamic(connection_id, envelope)
+ }
+
+ fn send_response(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
+ log::debug!(
+ "rpc respond. client_id:{}, name:{}",
+ self.id(),
+ message_type
+ );
+ let connection_id = self.connection_id()?;
+ self.peer.send_dynamic(connection_id, envelope)
+ }
+
+ fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet> {
+ &self.handler_set
}
}
@@ -2103,7 +1950,7 @@ mod tests {
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
- client.add_model_message_handler(
+ AnyProtoClient::from(client.clone()).add_model_message_handler(
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
match model.update(&mut cx, |model, _| model.id).unwrap() {
1 => done_tx1.try_send(()).unwrap(),
@@ -301,7 +301,7 @@ impl TestServer {
dev_server_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
language_model::LanguageModelRegistry::test(cx);
- assistant::context_store::init(&client);
+ assistant::context_store::init(&client.clone().into());
});
client
@@ -71,7 +71,7 @@ pub struct ProjectTransaction(pub HashMap<Model<Buffer>, language::Transaction>)
impl EventEmitter<BufferStoreEvent> for BufferStore {}
impl BufferStore {
- pub fn init(client: &Arc<Client>) {
+ pub fn init(client: &AnyProtoClient) {
client.add_model_message_handler(Self::handle_buffer_reloaded);
client.add_model_message_handler(Self::handle_buffer_saved);
client.add_model_message_handler(Self::handle_update_buffer_file);
@@ -12,7 +12,7 @@ use crate::{
};
use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
-use client::{proto, Client, TypedEnvelope};
+use client::{proto, TypedEnvelope};
use collections::{btree_map, BTreeMap, HashMap, HashSet};
use futures::{
future::{join_all, Shared},
@@ -45,6 +45,7 @@ use lsp::{
use parking_lot::{Mutex, RwLock};
use postage::watch;
use rand::prelude::*;
+
use rpc::proto::AnyProtoClient;
use serde::Serialize;
use settings::{Settings, SettingsLocation, SettingsStore};
@@ -84,51 +85,7 @@ const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);
-#[derive(Clone, Debug)]
-pub(crate) struct CoreSymbol {
- pub language_server_name: LanguageServerName,
- pub source_worktree_id: WorktreeId,
- pub path: ProjectPath,
- pub name: String,
- pub kind: lsp::SymbolKind,
- pub range: Range<Unclipped<PointUtf16>>,
- pub signature: [u8; 32],
-}
-
-pub enum LspStoreEvent {
- LanguageServerAdded(LanguageServerId),
- LanguageServerRemoved(LanguageServerId),
- LanguageServerUpdate {
- language_server_id: LanguageServerId,
- message: proto::update_language_server::Variant,
- },
- LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
- LanguageServerPrompt(LanguageServerPromptRequest),
- Notification(String),
- RefreshInlayHints,
- DiagnosticsUpdated {
- language_server_id: LanguageServerId,
- path: ProjectPath,
- },
- DiskBasedDiagnosticsStarted {
- language_server_id: LanguageServerId,
- },
- DiskBasedDiagnosticsFinished {
- language_server_id: LanguageServerId,
- },
- SnippetEdit {
- buffer_id: BufferId,
- edits: Vec<(lsp::Range, Snippet)>,
- most_recent_edit: clock::Lamport,
- },
- StartFormattingLocalBuffer(BufferId),
- FinishFormattingLocalBuffer(BufferId),
-}
-
-impl EventEmitter<LspStoreEvent> for LspStore {}
-
pub struct LspStore {
- _subscription: gpui::Subscription,
downstream_client: Option<AnyProtoClient>,
upstream_client: Option<AnyProtoClient>,
project_id: u64,
@@ -165,10 +122,60 @@ pub struct LspStore {
>,
>,
yarn: Model<YarnPathStore>,
+ _subscription: gpui::Subscription,
+}
+
+pub enum LspStoreEvent {
+ LanguageServerAdded(LanguageServerId),
+ LanguageServerRemoved(LanguageServerId),
+ LanguageServerUpdate {
+ language_server_id: LanguageServerId,
+ message: proto::update_language_server::Variant,
+ },
+ LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
+ LanguageServerPrompt(LanguageServerPromptRequest),
+ Notification(String),
+ RefreshInlayHints,
+ DiagnosticsUpdated {
+ language_server_id: LanguageServerId,
+ path: ProjectPath,
+ },
+ DiskBasedDiagnosticsStarted {
+ language_server_id: LanguageServerId,
+ },
+ DiskBasedDiagnosticsFinished {
+ language_server_id: LanguageServerId,
+ },
+ SnippetEdit {
+ buffer_id: BufferId,
+ edits: Vec<(lsp::Range, Snippet)>,
+ most_recent_edit: clock::Lamport,
+ },
+ StartFormattingLocalBuffer(BufferId),
+ FinishFormattingLocalBuffer(BufferId),
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct LanguageServerStatus {
+ pub name: String,
+ pub pending_work: BTreeMap<String, LanguageServerProgress>,
+ pub has_pending_diagnostic_updates: bool,
+ progress_tokens: HashSet<String>,
+}
+
+#[derive(Clone, Debug)]
+struct CoreSymbol {
+ pub language_server_name: LanguageServerName,
+ pub source_worktree_id: WorktreeId,
+ pub path: ProjectPath,
+ pub name: String,
+ pub kind: lsp::SymbolKind,
+ pub range: Range<Unclipped<PointUtf16>>,
+ pub signature: [u8; 32],
}
impl LspStore {
- pub fn init(client: &Arc<Client>) {
+ pub fn init(client: &AnyProtoClient) {
client.add_model_request_handler(Self::handle_multi_lsp_query);
client.add_model_request_handler(Self::handle_restart_language_servers);
client.add_model_message_handler(Self::handle_start_language_server);
@@ -180,6 +187,9 @@ impl LspStore {
client.add_model_request_handler(Self::handle_get_project_symbols);
client.add_model_request_handler(Self::handle_resolve_inlay_hint);
client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
+ client.add_model_request_handler(Self::handle_refresh_inlay_hints);
+ client.add_model_request_handler(Self::handle_on_type_formatting);
+ client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_lsp_command::<GetCodeActions>);
client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
@@ -192,10 +202,6 @@ impl LspStore {
client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
client.add_model_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
-
- client.add_model_request_handler(Self::handle_refresh_inlay_hints);
- client.add_model_request_handler(Self::handle_on_type_formatting);
- client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
}
#[allow(clippy::too_many_arguments)]
@@ -296,26 +302,6 @@ impl LspStore {
Ok(())
}
- fn send_lsp_proto_request<R: LspCommand>(
- &self,
- buffer: Model<Buffer>,
- project_id: u64,
- request: R,
- cx: &mut ModelContext<'_, Self>,
- ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
- let Some(upstream_client) = self.upstream_client.clone() else {
- return Task::ready(Err(anyhow!("disconnected before completing request")));
- };
- let message = request.to_proto(project_id, buffer.read(cx));
- cx.spawn(move |this, cx| async move {
- let response = upstream_client.request(message).await?;
- let this = this.upgrade().context("project dropped")?;
- request
- .response_from_proto(response, this, buffer, cx)
- .await
- })
- }
-
pub fn request_lsp<R: LspCommand>(
&self,
buffer_handle: Model<Buffer>,
@@ -416,6 +402,26 @@ impl LspStore {
Task::ready(Ok(Default::default()))
}
+ fn send_lsp_proto_request<R: LspCommand>(
+ &self,
+ buffer: Model<Buffer>,
+ project_id: u64,
+ request: R,
+ cx: &mut ModelContext<'_, Self>,
+ ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
+ let Some(upstream_client) = self.upstream_client.clone() else {
+ return Task::ready(Err(anyhow!("disconnected before completing request")));
+ };
+ let message = request.to_proto(project_id, buffer.read(cx));
+ cx.spawn(move |this, cx| async move {
+ let response = upstream_client.request(message).await?;
+ let this = this.upgrade().context("project dropped")?;
+ request
+ .response_from_proto(response, this, buffer, cx)
+ .await
+ })
+ }
+
pub async fn execute_code_actions_on_servers(
this: &WeakModel<LspStore>,
adapters_and_servers: &Vec<(Arc<CachedLspAdapter>, Arc<LanguageServer>)>,
@@ -440,7 +446,7 @@ impl LspStore {
.await?;
for mut action in actions {
- LspStore::try_resolve_code_action(&language_server, &mut action)
+ Self::try_resolve_code_action(&language_server, &mut action)
.await
.context("resolving a formatting code action")?;
@@ -490,7 +496,7 @@ impl LspStore {
Ok(())
}
- pub async fn try_resolve_code_action(
+ async fn try_resolve_code_action(
lang_server: &LanguageServer,
action: &mut CodeAction,
) -> anyhow::Result<()> {
@@ -507,63 +513,6 @@ impl LspStore {
anyhow::Ok(())
}
- pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
- proto::Completion {
- old_start: Some(serialize_anchor(&completion.old_range.start)),
- old_end: Some(serialize_anchor(&completion.old_range.end)),
- new_text: completion.new_text.clone(),
- server_id: completion.server_id.0 as u64,
- lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
- }
- }
-
- pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
- let old_start = completion
- .old_start
- .and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid old start"))?;
- let old_end = completion
- .old_end
- .and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid old end"))?;
- let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
-
- Ok(CoreCompletion {
- old_range: old_start..old_end,
- new_text: completion.new_text,
- server_id: LanguageServerId(completion.server_id as usize),
- lsp_completion,
- })
- }
-
- // todo: CodeAction.to_proto()
- pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
- proto::CodeAction {
- server_id: action.server_id.0 as u64,
- start: Some(serialize_anchor(&action.range.start)),
- end: Some(serialize_anchor(&action.range.end)),
- lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
- }
- }
-
- // todo: CodeAction::from__proto()
- pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction> {
- let start = action
- .start
- .and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid start"))?;
- let end = action
- .end
- .and_then(deserialize_anchor)
- .ok_or_else(|| anyhow!("invalid end"))?;
- let lsp_action = serde_json::from_slice(&action.lsp_action)?;
- Ok(CodeAction {
- server_id: LanguageServerId(action.server_id as usize),
- range: start..end,
- lsp_action,
- })
- }
-
pub fn apply_code_action(
&self,
buffer_handle: Model<Buffer>,
@@ -649,6 +598,66 @@ impl LspStore {
}
}
+ pub fn resolve_inlay_hint(
+ &self,
+ hint: InlayHint,
+ buffer_handle: Model<Buffer>,
+ server_id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<anyhow::Result<InlayHint>> {
+ if let Some(upstream_client) = self.upstream_client.clone() {
+ let request = proto::ResolveInlayHint {
+ project_id: self.project_id,
+ buffer_id: buffer_handle.read(cx).remote_id().into(),
+ language_server_id: server_id.0 as u64,
+ hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
+ };
+ cx.spawn(move |_, _| async move {
+ let response = upstream_client
+ .request(request)
+ .await
+ .context("inlay hints proto request")?;
+ match response.hint {
+ Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint)
+ .context("inlay hints proto resolve response conversion"),
+ None => Ok(hint),
+ }
+ })
+ } else {
+ let buffer = buffer_handle.read(cx);
+ let (_, lang_server) = if let Some((adapter, server)) =
+ self.language_server_for_buffer(buffer, server_id, cx)
+ {
+ (adapter.clone(), server.clone())
+ } else {
+ return Task::ready(Ok(hint));
+ };
+ if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) {
+ return Task::ready(Ok(hint));
+ }
+
+ let buffer_snapshot = buffer.snapshot();
+ cx.spawn(move |_, mut cx| async move {
+ let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
+ InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
+ );
+ let resolved_hint = resolve_task
+ .await
+ .context("inlay hint resolve LSP request")?;
+ let resolved_hint = InlayHints::lsp_to_project_hint(
+ resolved_hint,
+ &buffer_handle,
+ server_id,
+ ResolveState::Resolved,
+ false,
+ &mut cx,
+ )
+ .await?;
+ Ok(resolved_hint)
+ })
+ }
+ }
+
pub(crate) fn linked_edit(
&self,
buffer: &Model<Buffer>,
@@ -773,7 +782,7 @@ impl LspStore {
self.on_type_format_impl(buffer, position, trigger, push_to_history, cx)
}
- pub fn on_type_format_impl(
+ fn on_type_format_impl(
&mut self,
buffer: Model<Buffer>,
position: PointUtf16,
@@ -1715,36 +1724,6 @@ impl LspStore {
}
}
- pub(crate) fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
- let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
- let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
- let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
- let path = ProjectPath {
- worktree_id,
- path: PathBuf::from(serialized_symbol.path).into(),
- };
-
- let start = serialized_symbol
- .start
- .ok_or_else(|| anyhow!("invalid start"))?;
- let end = serialized_symbol
- .end
- .ok_or_else(|| anyhow!("invalid end"))?;
- Ok(CoreSymbol {
- language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()),
- source_worktree_id,
- path,
- name: serialized_symbol.name,
- range: Unclipped(PointUtf16::new(start.row, start.column))
- ..Unclipped(PointUtf16::new(end.row, end.column)),
- kind,
- signature: serialized_symbol
- .signature
- .try_into()
- .map_err(|_| anyhow!("invalid signature"))?,
- })
- }
-
pub fn diagnostic_summaries<'a>(
&'a self,
include_ignored: bool,
@@ -2332,7 +2311,7 @@ impl LspStore {
if let Some(client) = self.upstream_client.clone() {
let request = client.request(proto::OpenBufferForSymbol {
project_id: self.project_id,
- symbol: Some(serialize_symbol(symbol)),
+ symbol: Some(Self::serialize_symbol(symbol)),
});
cx.spawn(move |this, mut cx| async move {
let response = request.await?;
@@ -2343,13 +2322,10 @@ impl LspStore {
.await
})
} else {
- let language_server_id = if let Some(id) = self
- .language_server_id_for_worktree_and_name(
- symbol.source_worktree_id,
- symbol.language_server_name.clone(),
- ) {
- *id
- } else {
+ let Some(&language_server_id) = self.language_server_ids.get(&(
+ symbol.source_worktree_id,
+ symbol.language_server_name.clone(),
+ )) else {
return Task::ready(Err(anyhow!(
"language server for worktree and language not found"
)));
@@ -2587,7 +2563,7 @@ impl LspStore {
});
}
- pub async fn handle_lsp_command<T: LspCommand>(
+ async fn handle_lsp_command<T: LspCommand>(
this: Model<Self>,
envelope: TypedEnvelope<T::ProtoRequest>,
mut cx: AsyncAppContext,
@@ -2629,7 +2605,7 @@ impl LspStore {
})?
}
- pub async fn handle_multi_lsp_query(
+ async fn handle_multi_lsp_query(
this: Model<Self>,
envelope: TypedEnvelope<proto::MultiLspQuery>,
mut cx: AsyncAppContext,
@@ -2769,7 +2745,7 @@ impl LspStore {
}
}
- pub async fn handle_apply_code_action(
+ async fn handle_apply_code_action(
this: Model<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,
mut cx: AsyncAppContext,
@@ -2802,7 +2778,7 @@ impl LspStore {
})
}
- pub async fn handle_update_diagnostic_summary(
+ async fn handle_update_diagnostic_summary(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
mut cx: AsyncAppContext,
@@ -2849,7 +2825,7 @@ impl LspStore {
})?
}
- pub async fn handle_start_language_server(
+ async fn handle_start_language_server(
this: Model<Self>,
envelope: TypedEnvelope<proto::StartLanguageServer>,
mut cx: AsyncAppContext,
@@ -2873,7 +2849,7 @@ impl LspStore {
Ok(())
}
- pub async fn handle_update_language_server(
+ async fn handle_update_language_server(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateLanguageServer>,
mut cx: AsyncAppContext,
@@ -3094,14 +3070,6 @@ impl LspStore {
cx.notify();
}
- pub fn language_server_id_for_worktree_and_name(
- &self,
- worktree_id: WorktreeId,
- name: LanguageServerName,
- ) -> Option<&LanguageServerId> {
- self.language_server_ids.get(&(worktree_id, name))
- }
-
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
Some(server.clone())
@@ -3112,480 +3080,225 @@ impl LspStore {
}
}
- pub async fn deserialize_text_edits(
- this: Model<Self>,
- buffer_to_edit: Model<Buffer>,
- edits: Vec<lsp::TextEdit>,
- push_to_history: bool,
- _: Arc<CachedLspAdapter>,
- language_server: Arc<LanguageServer>,
- cx: &mut AsyncAppContext,
- ) -> Result<Option<Transaction>> {
- let edits = this
- .update(cx, |this, cx| {
- this.edits_from_lsp(
- &buffer_to_edit,
- edits,
- language_server.server_id(),
- None,
- cx,
- )
- })?
- .await?;
+ async fn on_lsp_workspace_edit(
+ this: WeakModel<Self>,
+ params: lsp::ApplyWorkspaceEditParams,
+ server_id: LanguageServerId,
+ adapter: Arc<CachedLspAdapter>,
+ mut cx: AsyncAppContext,
+ ) -> Result<lsp::ApplyWorkspaceEditResponse> {
+ let this = this
+ .upgrade()
+ .ok_or_else(|| anyhow!("project project closed"))?;
+ let language_server = this
+ .update(&mut cx, |this, _| this.language_server_for_id(server_id))?
+ .ok_or_else(|| anyhow!("language server not found"))?;
+ let transaction = Self::deserialize_workspace_edit(
+ this.clone(),
+ params.edit,
+ true,
+ adapter.clone(),
+ language_server.clone(),
+ &mut cx,
+ )
+ .await
+ .log_err();
+ this.update(&mut cx, |this, _| {
+ if let Some(transaction) = transaction {
+ this.last_workspace_edits_by_language_server
+ .insert(server_id, transaction);
+ }
+ })?;
+ Ok(lsp::ApplyWorkspaceEditResponse {
+ applied: true,
+ failed_change: None,
+ failure_reason: None,
+ })
+ }
- let transaction = buffer_to_edit.update(cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.start_transaction();
- for (range, text) in edits {
- buffer.edit([(range, text)], None, cx);
+ fn on_lsp_progress(
+ &mut self,
+ progress: lsp::ProgressParams,
+ language_server_id: LanguageServerId,
+ disk_based_diagnostics_progress_token: Option<String>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let token = match progress.token {
+ lsp::NumberOrString::String(token) => token,
+ lsp::NumberOrString::Number(token) => {
+ log::info!("skipping numeric progress token {}", token);
+ return;
}
+ };
- if buffer.end_transaction(cx).is_some() {
- let transaction = buffer.finalize_last_transaction().unwrap().clone();
- if !push_to_history {
- buffer.forget_transaction(transaction.id);
- }
- Some(transaction)
+ let lsp::ProgressParamsValue::WorkDone(progress) = progress.value;
+ let language_server_status =
+ if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+ status
} else {
- None
- }
- })?;
+ return;
+ };
- Ok(transaction)
- }
+ if !language_server_status.progress_tokens.contains(&token) {
+ return;
+ }
- pub async fn deserialize_workspace_edit(
- this: Model<Self>,
- edit: lsp::WorkspaceEdit,
- push_to_history: bool,
- lsp_adapter: Arc<CachedLspAdapter>,
- language_server: Arc<LanguageServer>,
- cx: &mut AsyncAppContext,
- ) -> Result<ProjectTransaction> {
- let fs = this.update(cx, |this, _| this.fs.clone())?;
- let mut operations = Vec::new();
- if let Some(document_changes) = edit.document_changes {
- match document_changes {
- lsp::DocumentChanges::Edits(edits) => {
- operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit))
+ let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token
+ .as_ref()
+ .map_or(false, |disk_based_token| {
+ token.starts_with(disk_based_token)
+ });
+
+ match progress {
+ lsp::WorkDoneProgress::Begin(report) => {
+ if is_disk_based_diagnostics_progress {
+ self.disk_based_diagnostics_started(language_server_id, cx);
}
- lsp::DocumentChanges::Operations(ops) => operations = ops,
+ self.on_lsp_work_start(
+ language_server_id,
+ token.clone(),
+ LanguageServerProgress {
+ title: Some(report.title),
+ is_disk_based_diagnostics_progress,
+ is_cancellable: report.cancellable.unwrap_or(false),
+ message: report.message.clone(),
+ percentage: report.percentage.map(|p| p as usize),
+ last_update_at: cx.background_executor().now(),
+ },
+ cx,
+ );
}
- } else if let Some(changes) = edit.changes {
- operations.extend(changes.into_iter().map(|(uri, edits)| {
- lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
- text_document: lsp::OptionalVersionedTextDocumentIdentifier {
- uri,
- version: None,
+ lsp::WorkDoneProgress::Report(report) => {
+ if self.on_lsp_work_progress(
+ language_server_id,
+ token.clone(),
+ LanguageServerProgress {
+ title: None,
+ is_disk_based_diagnostics_progress,
+ is_cancellable: report.cancellable.unwrap_or(false),
+ message: report.message.clone(),
+ percentage: report.percentage.map(|p| p as usize),
+ last_update_at: cx.background_executor().now(),
},
- edits: edits.into_iter().map(Edit::Plain).collect(),
- })
- }));
- }
-
- let mut project_transaction = ProjectTransaction::default();
- for operation in operations {
- match operation {
- lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => {
- let abs_path = op
- .uri
- .to_file_path()
- .map_err(|_| anyhow!("can't convert URI to path"))?;
-
- if let Some(parent_path) = abs_path.parent() {
- fs.create_dir(parent_path).await?;
- }
- if abs_path.ends_with("/") {
- fs.create_dir(&abs_path).await?;
- } else {
- fs.create_file(
- &abs_path,
- op.options
- .map(|options| fs::CreateOptions {
- overwrite: options.overwrite.unwrap_or(false),
- ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
- })
- .unwrap_or_default(),
- )
- .await?;
- }
+ cx,
+ ) {
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ message: proto::update_language_server::Variant::WorkProgress(
+ proto::LspWorkProgress {
+ token,
+ message: report.message,
+ percentage: report.percentage,
+ },
+ ),
+ })
}
-
- lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => {
- let source_abs_path = op
- .old_uri
- .to_file_path()
- .map_err(|_| anyhow!("can't convert URI to path"))?;
- let target_abs_path = op
- .new_uri
- .to_file_path()
- .map_err(|_| anyhow!("can't convert URI to path"))?;
- fs.rename(
- &source_abs_path,
- &target_abs_path,
- op.options
- .map(|options| fs::RenameOptions {
- overwrite: options.overwrite.unwrap_or(false),
- ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
- })
- .unwrap_or_default(),
- )
- .await?;
+ }
+ lsp::WorkDoneProgress::End(_) => {
+ language_server_status.progress_tokens.remove(&token);
+ self.on_lsp_work_end(language_server_id, token.clone(), cx);
+ if is_disk_based_diagnostics_progress {
+ self.disk_based_diagnostics_finished(language_server_id, cx);
}
+ }
+ }
+ }
- lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => {
- let abs_path = op
- .uri
- .to_file_path()
- .map_err(|_| anyhow!("can't convert URI to path"))?;
- let options = op
- .options
- .map(|options| fs::RemoveOptions {
- recursive: options.recursive.unwrap_or(false),
- ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
- })
- .unwrap_or_default();
- if abs_path.ends_with("/") {
- fs.remove_dir(&abs_path, options).await?;
- } else {
- fs.remove_file(&abs_path, options).await?;
- }
- }
-
- lsp::DocumentChangeOperation::Edit(op) => {
- let buffer_to_edit = this
- .update(cx, |this, cx| {
- this.open_local_buffer_via_lsp(
- op.text_document.uri.clone(),
- language_server.server_id(),
- lsp_adapter.name.clone(),
- cx,
- )
- })?
- .await?;
-
- let edits = this
- .update(cx, |this, cx| {
- let path = buffer_to_edit.read(cx).project_path(cx);
- let active_entry = this.active_entry;
- let is_active_entry = path.clone().map_or(false, |project_path| {
- this.worktree_store
- .read(cx)
- .entry_for_path(&project_path, cx)
- .map_or(false, |entry| Some(entry.id) == active_entry)
- });
-
- let (mut edits, mut snippet_edits) = (vec![], vec![]);
- for edit in op.edits {
- match edit {
- Edit::Plain(edit) => edits.push(edit),
- Edit::Annotated(edit) => edits.push(edit.text_edit),
- Edit::Snippet(edit) => {
- let Ok(snippet) = Snippet::parse(&edit.snippet.value)
- else {
- continue;
- };
-
- if is_active_entry {
- snippet_edits.push((edit.range, snippet));
- } else {
- // Since this buffer is not focused, apply a normal edit.
- edits.push(TextEdit {
- range: edit.range,
- new_text: snippet.text,
- });
- }
- }
- }
- }
- if !snippet_edits.is_empty() {
- if let Some(buffer_version) = op.text_document.version {
- let buffer_id = buffer_to_edit.read(cx).remote_id();
- // Check if the edit that triggered that edit has been made by this participant.
- let most_recent_edit = this
- .buffer_snapshots
- .get(&buffer_id)
- .and_then(|server_to_snapshots| {
- let all_snapshots = server_to_snapshots
- .get(&language_server.server_id())?;
- all_snapshots
- .binary_search_by_key(&buffer_version, |snapshot| {
- snapshot.version
- })
- .ok()
- .and_then(|index| all_snapshots.get(index))
- })
- .and_then(|lsp_snapshot| {
- let version = lsp_snapshot.snapshot.version();
- version.iter().max_by_key(|timestamp| timestamp.value)
- });
- if let Some(most_recent_edit) = most_recent_edit {
- cx.emit(LspStoreEvent::SnippetEdit {
- buffer_id,
- edits: snippet_edits,
- most_recent_edit,
- });
- }
- }
- }
-
- this.edits_from_lsp(
- &buffer_to_edit,
- edits,
- language_server.server_id(),
- op.text_document.version,
- cx,
- )
- })?
- .await?;
+ fn on_lsp_work_start(
+ &mut self,
+ language_server_id: LanguageServerId,
+ token: String,
+ progress: LanguageServerProgress,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+ status.pending_work.insert(token.clone(), progress.clone());
+ cx.notify();
+ }
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
+ token,
+ title: progress.title,
+ message: progress.message,
+ percentage: progress.percentage.map(|p| p as u32),
+ }),
+ })
+ }
- let transaction = buffer_to_edit.update(cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.start_transaction();
- for (range, text) in edits {
- buffer.edit([(range, text)], None, cx);
+ fn on_lsp_work_progress(
+ &mut self,
+ language_server_id: LanguageServerId,
+ token: String,
+ progress: LanguageServerProgress,
+ cx: &mut ModelContext<Self>,
+ ) -> bool {
+ if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+ match status.pending_work.entry(token) {
+ btree_map::Entry::Vacant(entry) => {
+ entry.insert(progress);
+ cx.notify();
+ return true;
+ }
+ btree_map::Entry::Occupied(mut entry) => {
+ let entry = entry.get_mut();
+ if (progress.last_update_at - entry.last_update_at)
+ >= SERVER_PROGRESS_THROTTLE_TIMEOUT
+ {
+ entry.last_update_at = progress.last_update_at;
+ if progress.message.is_some() {
+ entry.message = progress.message;
}
- let transaction = if buffer.end_transaction(cx).is_some() {
- let transaction = buffer.finalize_last_transaction().unwrap().clone();
- if !push_to_history {
- buffer.forget_transaction(transaction.id);
- }
- Some(transaction)
- } else {
- None
- };
-
- transaction
- })?;
- if let Some(transaction) = transaction {
- project_transaction.0.insert(buffer_to_edit, transaction);
+ if progress.percentage.is_some() {
+ entry.percentage = progress.percentage;
+ }
+ cx.notify();
+ return true;
}
}
}
}
- Ok(project_transaction)
+ false
}
- async fn on_lsp_workspace_edit(
- this: WeakModel<Self>,
- params: lsp::ApplyWorkspaceEditParams,
- server_id: LanguageServerId,
- adapter: Arc<CachedLspAdapter>,
- mut cx: AsyncAppContext,
- ) -> Result<lsp::ApplyWorkspaceEditResponse> {
- let this = this
- .upgrade()
- .ok_or_else(|| anyhow!("project project closed"))?;
- let language_server = this
- .update(&mut cx, |this, _| this.language_server_for_id(server_id))?
- .ok_or_else(|| anyhow!("language server not found"))?;
- let transaction = Self::deserialize_workspace_edit(
- this.clone(),
- params.edit,
- true,
- adapter.clone(),
- language_server.clone(),
- &mut cx,
- )
- .await
- .log_err();
- this.update(&mut cx, |this, _| {
- if let Some(transaction) = transaction {
- this.last_workspace_edits_by_language_server
- .insert(server_id, transaction);
+ fn on_lsp_work_end(
+ &mut self,
+ language_server_id: LanguageServerId,
+ token: String,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+ if let Some(work) = status.pending_work.remove(&token) {
+ if !work.is_disk_based_diagnostics_progress {
+ cx.emit(LspStoreEvent::RefreshInlayHints);
+ }
}
- })?;
- Ok(lsp::ApplyWorkspaceEditResponse {
- applied: true,
- failed_change: None,
- failure_reason: None,
+ cx.notify();
+ }
+
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { token }),
})
}
- fn on_lsp_progress(
+ fn on_lsp_did_change_watched_files(
&mut self,
- progress: lsp::ProgressParams,
language_server_id: LanguageServerId,
- disk_based_diagnostics_progress_token: Option<String>,
+ registration_id: &str,
+ params: DidChangeWatchedFilesRegistrationOptions,
cx: &mut ModelContext<Self>,
) {
- let token = match progress.token {
- lsp::NumberOrString::String(token) => token,
- lsp::NumberOrString::Number(token) => {
- log::info!("skipping numeric progress token {}", token);
- return;
- }
- };
+ let registrations = self
+ .language_server_watcher_registrations
+ .entry(language_server_id)
+ .or_default();
- let lsp::ProgressParamsValue::WorkDone(progress) = progress.value;
- let language_server_status =
- if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
- status
- } else {
- return;
- };
+ registrations.insert(registration_id.to_string(), params.watchers);
- if !language_server_status.progress_tokens.contains(&token) {
- return;
- }
-
- let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token
- .as_ref()
- .map_or(false, |disk_based_token| {
- token.starts_with(disk_based_token)
- });
-
- match progress {
- lsp::WorkDoneProgress::Begin(report) => {
- if is_disk_based_diagnostics_progress {
- self.disk_based_diagnostics_started(language_server_id, cx);
- }
- self.on_lsp_work_start(
- language_server_id,
- token.clone(),
- LanguageServerProgress {
- title: Some(report.title),
- is_disk_based_diagnostics_progress,
- is_cancellable: report.cancellable.unwrap_or(false),
- message: report.message.clone(),
- percentage: report.percentage.map(|p| p as usize),
- last_update_at: cx.background_executor().now(),
- },
- cx,
- );
- }
- lsp::WorkDoneProgress::Report(report) => {
- if self.on_lsp_work_progress(
- language_server_id,
- token.clone(),
- LanguageServerProgress {
- title: None,
- is_disk_based_diagnostics_progress,
- is_cancellable: report.cancellable.unwrap_or(false),
- message: report.message.clone(),
- percentage: report.percentage.map(|p| p as usize),
- last_update_at: cx.background_executor().now(),
- },
- cx,
- ) {
- cx.emit(LspStoreEvent::LanguageServerUpdate {
- language_server_id,
- message: proto::update_language_server::Variant::WorkProgress(
- proto::LspWorkProgress {
- token,
- message: report.message,
- percentage: report.percentage,
- },
- ),
- })
- }
- }
- lsp::WorkDoneProgress::End(_) => {
- language_server_status.progress_tokens.remove(&token);
- self.on_lsp_work_end(language_server_id, token.clone(), cx);
- if is_disk_based_diagnostics_progress {
- self.disk_based_diagnostics_finished(language_server_id, cx);
- }
- }
- }
- }
-
- fn on_lsp_work_start(
- &mut self,
- language_server_id: LanguageServerId,
- token: String,
- progress: LanguageServerProgress,
- cx: &mut ModelContext<Self>,
- ) {
- if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
- status.pending_work.insert(token.clone(), progress.clone());
- cx.notify();
- }
- cx.emit(LspStoreEvent::LanguageServerUpdate {
- language_server_id,
- message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
- token,
- title: progress.title,
- message: progress.message,
- percentage: progress.percentage.map(|p| p as u32),
- }),
- })
- }
-
- fn on_lsp_work_progress(
- &mut self,
- language_server_id: LanguageServerId,
- token: String,
- progress: LanguageServerProgress,
- cx: &mut ModelContext<Self>,
- ) -> bool {
- if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
- match status.pending_work.entry(token) {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(progress);
- cx.notify();
- return true;
- }
- btree_map::Entry::Occupied(mut entry) => {
- let entry = entry.get_mut();
- if (progress.last_update_at - entry.last_update_at)
- >= SERVER_PROGRESS_THROTTLE_TIMEOUT
- {
- entry.last_update_at = progress.last_update_at;
- if progress.message.is_some() {
- entry.message = progress.message;
- }
- if progress.percentage.is_some() {
- entry.percentage = progress.percentage;
- }
- cx.notify();
- return true;
- }
- }
- }
- }
-
- false
- }
-
- fn on_lsp_work_end(
- &mut self,
- language_server_id: LanguageServerId,
- token: String,
- cx: &mut ModelContext<Self>,
- ) {
- if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
- if let Some(work) = status.pending_work.remove(&token) {
- if !work.is_disk_based_diagnostics_progress {
- cx.emit(LspStoreEvent::RefreshInlayHints);
- }
- }
- cx.notify();
- }
-
- cx.emit(LspStoreEvent::LanguageServerUpdate {
- language_server_id,
- message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { token }),
- })
- }
-
- fn on_lsp_did_change_watched_files(
- &mut self,
- language_server_id: LanguageServerId,
- registration_id: &str,
- params: DidChangeWatchedFilesRegistrationOptions,
- cx: &mut ModelContext<Self>,
- ) {
- let registrations = self
- .language_server_watcher_registrations
- .entry(language_server_id)
- .or_default();
-
- registrations.insert(registration_id.to_string(), params.watchers);
-
- self.rebuild_watched_paths(language_server_id, cx);
- }
+ self.rebuild_watched_paths(language_server_id, cx);
+ }
fn on_lsp_unregister_did_change_watched_files(
&mut self,
@@ -65,7 +65,10 @@ use paths::{
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
use remote::SshSession;
-use rpc::{proto::AnyProtoClient, ErrorCode};
+use rpc::{
+ proto::{AnyProtoClient, SSH_PROJECT_ID},
+ ErrorCode,
+};
use search::{SearchQuery, SearchResult};
use search_history::SearchHistory;
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
@@ -574,6 +577,7 @@ impl Project {
connection_manager::init(client.clone(), cx);
Self::init_settings(cx);
+ let client: AnyProtoClient = client.clone().into();
client.add_model_message_handler(Self::handle_add_collaborator);
client.add_model_message_handler(Self::handle_update_project_collaborator);
client.add_model_message_handler(Self::handle_remove_collaborator);
@@ -594,9 +598,9 @@ impl Project {
client.add_model_request_handler(Self::handle_task_templates);
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
- WorktreeStore::init(client);
- BufferStore::init(client);
- LspStore::init(client);
+ WorktreeStore::init(&client);
+ BufferStore::init(&client);
+ LspStore::init(&client);
}
pub fn local(
@@ -697,15 +701,19 @@ impl Project {
) -> Model<Self> {
let this = Self::local(client, node, user_store, languages, fs, None, cx);
this.update(cx, |this, cx| {
- let buffer_store = this.buffer_store.downgrade();
+ let client: AnyProtoClient = ssh.clone().into();
+
this.worktree_store.update(cx, |store, _cx| {
- store.set_upstream_client(ssh.clone().into());
+ store.set_upstream_client(client.clone());
});
- ssh.add_message_handler(cx.weak_model(), Self::handle_update_worktree);
- ssh.add_message_handler(cx.weak_model(), Self::handle_create_buffer_for_peer);
- ssh.add_message_handler(buffer_store.clone(), BufferStore::handle_update_buffer_file);
- ssh.add_message_handler(buffer_store.clone(), BufferStore::handle_update_diff_base);
+ ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
+ ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
+ ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
+ client.add_model_message_handler(Self::handle_update_worktree);
+ client.add_model_message_handler(Self::handle_create_buffer_for_peer);
+ client.add_model_message_handler(BufferStore::handle_update_buffer_file);
+ client.add_model_message_handler(BufferStore::handle_update_diff_base);
this.ssh_session = Some(ssh);
});
@@ -5,7 +5,7 @@ use std::{
};
use anyhow::{anyhow, Context as _, Result};
-use client::{Client, DevServerProjectId};
+use client::DevServerProjectId;
use collections::{HashMap, HashSet};
use fs::Fs;
use futures::{
@@ -17,7 +17,7 @@ use gpui::{
};
use postage::oneshot;
use rpc::{
- proto::{self, AnyProtoClient},
+ proto::{self, AnyProtoClient, SSH_PROJECT_ID},
TypedEnvelope,
};
use smol::{
@@ -58,12 +58,12 @@ pub enum WorktreeStoreEvent {
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
impl WorktreeStore {
- pub fn init(client: &Arc<Client>) {
- client.add_model_request_handler(WorktreeStore::handle_create_project_entry);
- client.add_model_request_handler(WorktreeStore::handle_rename_project_entry);
- client.add_model_request_handler(WorktreeStore::handle_copy_project_entry);
- client.add_model_request_handler(WorktreeStore::handle_delete_project_entry);
- client.add_model_request_handler(WorktreeStore::handle_expand_project_entry);
+ pub fn init(client: &AnyProtoClient) {
+ client.add_model_request_handler(Self::handle_create_project_entry);
+ client.add_model_request_handler(Self::handle_rename_project_entry);
+ client.add_model_request_handler(Self::handle_copy_project_entry);
+ client.add_model_request_handler(Self::handle_delete_project_entry);
+ client.add_model_request_handler(Self::handle_expand_project_entry);
}
pub fn new(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
@@ -188,7 +188,10 @@ impl WorktreeStore {
let path = abs_path.to_string_lossy().to_string();
cx.spawn(|this, mut cx| async move {
let response = client
- .request(proto::AddWorktree { path: path.clone() })
+ .request(proto::AddWorktree {
+ project_id: SSH_PROJECT_ID,
+ path: path.clone(),
+ })
.await?;
let worktree = cx.update(|cx| {
Worktree::remote(
@@ -20,8 +20,10 @@ doctest = false
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
+parking_lot.workspace = true
prost.workspace = true
serde.workspace = true
+gpui.workspace = true
[build-dependencies]
prost-build.workspace = true
@@ -2484,6 +2484,7 @@ message GetLlmTokenResponse {
// Remote FS
message AddWorktree {
+ uint64 project_id = 2;
string path = 1;
}
@@ -2,14 +2,14 @@
pub mod error;
mod macros;
+mod proto_client;
mod typed_envelope;
pub use error::*;
+pub use proto_client::*;
pub use typed_envelope::*;
-use anyhow::anyhow;
use collections::HashMap;
-use futures::{future::BoxFuture, Future};
pub use prost::{DecodeError, Message};
use serde::Serialize;
use std::{
@@ -17,12 +17,14 @@ use std::{
cmp,
fmt::{self, Debug},
iter, mem,
- sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
+pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
+pub const SSH_PROJECT_ID: u64 = 0;
+
pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static {
const NAME: &'static str;
const PRIORITY: MessagePriority;
@@ -60,51 +62,6 @@ pub enum MessagePriority {
Background,
}
-pub trait ProtoClient: Send + Sync {
- fn request(
- &self,
- envelope: Envelope,
- request_type: &'static str,
- ) -> BoxFuture<'static, anyhow::Result<Envelope>>;
-
- fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>;
-}
-
-#[derive(Clone)]
-pub struct AnyProtoClient(Arc<dyn ProtoClient>);
-
-impl<T> From<Arc<T>> for AnyProtoClient
-where
- T: ProtoClient + 'static,
-{
- fn from(client: Arc<T>) -> Self {
- Self(client)
- }
-}
-
-impl AnyProtoClient {
- pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
- Self(client)
- }
-
- pub fn request<T: RequestMessage>(
- &self,
- request: T,
- ) -> impl Future<Output = anyhow::Result<T::Response>> {
- let envelope = request.into_envelope(0, None, None);
- let response = self.0.request(envelope, T::NAME);
- async move {
- T::Response::from_envelope(response.await?)
- .ok_or_else(|| anyhow!("received response of the wrong type"))
- }
- }
-
- pub fn send<T: EnvelopedMessage>(&self, request: T) -> anyhow::Result<()> {
- let envelope = request.into_envelope(0, None, None);
- self.0.send(envelope, T::NAME)
- }
-}
-
impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
fn payload_type_id(&self) -> TypeId {
TypeId::of::<T>()
@@ -537,11 +494,13 @@ request_messages!(
entity_messages!(
{project_id, ShareProject},
AddProjectCollaborator,
+ AddWorktree,
ApplyCodeAction,
ApplyCompletionAdditionalEdits,
BlameBuffer,
BufferReloaded,
BufferSaved,
+ CloseBuffer,
CopyProjectEntry,
CreateBufferForPeer,
CreateProjectEntry,
@@ -0,0 +1,277 @@
+use crate::{
+ error::ErrorExt as _, AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage,
+ RequestMessage, TypedEnvelope,
+};
+use anyhow::anyhow;
+use collections::HashMap;
+use futures::{
+ future::{BoxFuture, LocalBoxFuture},
+ Future, FutureExt as _,
+};
+use gpui::{AnyModel, AnyWeakModel, AsyncAppContext, Model};
+pub use prost::Message;
+use std::{any::TypeId, sync::Arc};
+
+#[derive(Clone)]
+pub struct AnyProtoClient(Arc<dyn ProtoClient>);
+
+pub trait ProtoClient: Send + Sync {
+ fn request(
+ &self,
+ envelope: Envelope,
+ request_type: &'static str,
+ ) -> BoxFuture<'static, anyhow::Result<Envelope>>;
+
+ fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>;
+
+ fn send_response(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>;
+
+ fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet>;
+}
+
+#[derive(Default)]
+pub struct ProtoMessageHandlerSet {
+ pub entity_types_by_message_type: HashMap<TypeId, TypeId>,
+ pub entities_by_type_and_remote_id: HashMap<(TypeId, u64), EntityMessageSubscriber>,
+ pub entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
+ pub models_by_message_type: HashMap<TypeId, AnyWeakModel>,
+ pub message_handlers: HashMap<TypeId, ProtoMessageHandler>,
+}
+
+pub type ProtoMessageHandler = Arc<
+ dyn Send
+ + Sync
+ + Fn(
+ AnyModel,
+ Box<dyn AnyTypedEnvelope>,
+ AnyProtoClient,
+ AsyncAppContext,
+ ) -> LocalBoxFuture<'static, anyhow::Result<()>>,
+>;
+
+impl ProtoMessageHandlerSet {
+ pub fn clear(&mut self) {
+ self.message_handlers.clear();
+ self.models_by_message_type.clear();
+ self.entities_by_type_and_remote_id.clear();
+ self.entity_id_extractors.clear();
+ }
+
+ fn add_message_handler(
+ &mut self,
+ message_type_id: TypeId,
+ model: gpui::AnyWeakModel,
+ handler: ProtoMessageHandler,
+ ) {
+ self.models_by_message_type.insert(message_type_id, model);
+ let prev_handler = self.message_handlers.insert(message_type_id, handler);
+ if prev_handler.is_some() {
+ panic!("registered handler for the same message twice");
+ }
+ }
+
+ fn add_entity_message_handler(
+ &mut self,
+ message_type_id: TypeId,
+ model_type_id: TypeId,
+ entity_id_extractor: fn(&dyn AnyTypedEnvelope) -> u64,
+ handler: ProtoMessageHandler,
+ ) {
+ self.entity_id_extractors
+ .entry(message_type_id)
+ .or_insert(entity_id_extractor);
+ self.entity_types_by_message_type
+ .insert(message_type_id, model_type_id);
+ let prev_handler = self.message_handlers.insert(message_type_id, handler);
+ if prev_handler.is_some() {
+ panic!("registered handler for the same message twice");
+ }
+ }
+
+ pub fn handle_message(
+ this: &parking_lot::Mutex<Self>,
+ message: Box<dyn AnyTypedEnvelope>,
+ client: AnyProtoClient,
+ cx: AsyncAppContext,
+ ) -> Option<LocalBoxFuture<'static, anyhow::Result<()>>> {
+ let payload_type_id = message.payload_type_id();
+ let mut this = this.lock();
+ let handler = this.message_handlers.get(&payload_type_id)?.clone();
+ let entity = if let Some(entity) = this.models_by_message_type.get(&payload_type_id) {
+ entity.upgrade()?
+ } else {
+ let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?;
+ let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?;
+ let entity_id = (extract_entity_id)(message.as_ref());
+
+ match this
+ .entities_by_type_and_remote_id
+ .get_mut(&(entity_type_id, entity_id))?
+ {
+ EntityMessageSubscriber::Pending(pending) => {
+ pending.push(message);
+ return None;
+ }
+ EntityMessageSubscriber::Entity { handle } => handle.upgrade()?,
+ }
+ };
+ drop(this);
+ Some(handler(entity, message, client, cx))
+ }
+}
+
+pub enum EntityMessageSubscriber {
+ Entity { handle: AnyWeakModel },
+ Pending(Vec<Box<dyn AnyTypedEnvelope>>),
+}
+
+impl<T> From<Arc<T>> for AnyProtoClient
+where
+ T: ProtoClient + 'static,
+{
+ fn from(client: Arc<T>) -> Self {
+ Self(client)
+ }
+}
+
+impl AnyProtoClient {
+ pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
+ Self(client)
+ }
+
+ pub fn request<T: RequestMessage>(
+ &self,
+ request: T,
+ ) -> impl Future<Output = anyhow::Result<T::Response>> {
+ let envelope = request.into_envelope(0, None, None);
+ let response = self.0.request(envelope, T::NAME);
+ async move {
+ T::Response::from_envelope(response.await?)
+ .ok_or_else(|| anyhow!("received response of the wrong type"))
+ }
+ }
+
+ pub fn send<T: EnvelopedMessage>(&self, request: T) -> anyhow::Result<()> {
+ let envelope = request.into_envelope(0, None, None);
+ self.0.send(envelope, T::NAME)
+ }
+
+ pub fn send_response<T: EnvelopedMessage>(
+ &self,
+ request_id: u32,
+ request: T,
+ ) -> anyhow::Result<()> {
+ let envelope = request.into_envelope(0, Some(request_id), None);
+ self.0.send(envelope, T::NAME)
+ }
+
+ pub fn add_request_handler<M, E, H, F>(&self, model: gpui::WeakModel<E>, handler: H)
+ where
+ M: RequestMessage,
+ E: 'static,
+ H: 'static + Sync + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
+ F: 'static + Future<Output = anyhow::Result<M::Response>>,
+ {
+ self.0.message_handler_set().lock().add_message_handler(
+ TypeId::of::<M>(),
+ model.into(),
+ Arc::new(move |model, envelope, client, cx| {
+ let model = model.downcast::<E>().unwrap();
+ let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+ let request_id = envelope.message_id();
+ handler(model, *envelope, cx)
+ .then(move |result| async move {
+ match result {
+ Ok(response) => {
+ client.send_response(request_id, response)?;
+ Ok(())
+ }
+ Err(error) => {
+ client.send_response(request_id, error.to_proto())?;
+ Err(error)
+ }
+ }
+ })
+ .boxed_local()
+ }),
+ )
+ }
+
+ pub fn add_model_request_handler<M, E, H, F>(&self, handler: H)
+ where
+ M: EnvelopedMessage + RequestMessage + EntityMessage,
+ E: 'static,
+ H: 'static + Sync + Send + Fn(gpui::Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
+ F: 'static + Future<Output = anyhow::Result<M::Response>>,
+ {
+ let message_type_id = TypeId::of::<M>();
+ let model_type_id = TypeId::of::<E>();
+ let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| {
+ envelope
+ .as_any()
+ .downcast_ref::<TypedEnvelope<M>>()
+ .unwrap()
+ .payload
+ .remote_entity_id()
+ };
+ self.0
+ .message_handler_set()
+ .lock()
+ .add_entity_message_handler(
+ message_type_id,
+ model_type_id,
+ entity_id_extractor,
+ Arc::new(move |model, envelope, client, cx| {
+ let model = model.downcast::<E>().unwrap();
+ let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+ let request_id = envelope.message_id();
+ handler(model, *envelope, cx)
+ .then(move |result| async move {
+ match result {
+ Ok(response) => {
+ client.send_response(request_id, response)?;
+ Ok(())
+ }
+ Err(error) => {
+ client.send_response(request_id, error.to_proto())?;
+ Err(error)
+ }
+ }
+ })
+ .boxed_local()
+ }),
+ );
+ }
+
+ pub fn add_model_message_handler<M, E, H, F>(&self, handler: H)
+ where
+ M: EnvelopedMessage + EntityMessage,
+ E: 'static,
+ H: 'static + Sync + Send + Fn(gpui::Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
+ F: 'static + Future<Output = anyhow::Result<()>>,
+ {
+ let message_type_id = TypeId::of::<M>();
+ let model_type_id = TypeId::of::<E>();
+ let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| {
+ envelope
+ .as_any()
+ .downcast_ref::<TypedEnvelope<M>>()
+ .unwrap()
+ .payload
+ .remote_entity_id()
+ };
+ self.0
+ .message_handler_set()
+ .lock()
+ .add_entity_message_handler(
+ message_type_id,
+ model_type_id,
+ entity_id_extractor,
+ Arc::new(move |model, envelope, _, cx| {
+ let model = model.downcast::<E>().unwrap();
+ let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+ handler(model, *envelope, cx).boxed_local()
+ }),
+ );
+ }
+}
@@ -8,17 +8,14 @@ use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
use futures::{
channel::{mpsc, oneshot},
- future::{BoxFuture, LocalBoxFuture},
+ future::BoxFuture,
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, StreamExt as _,
};
-use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, WeakModel};
+use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion};
use parking_lot::Mutex;
-use rpc::{
- proto::{
- self, build_typed_envelope, AnyTypedEnvelope, Envelope, EnvelopedMessage, PeerId,
- ProtoClient, RequestMessage,
- },
- TypedEnvelope,
+use rpc::proto::{
+ self, build_typed_envelope, EntityMessageSubscriber, Envelope, EnvelopedMessage, PeerId,
+ ProtoClient, ProtoMessageHandlerSet, RequestMessage,
};
use smol::{
fs,
@@ -48,20 +45,7 @@ pub struct SshSession {
outgoing_tx: mpsc::UnboundedSender<Envelope>,
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
client_socket: Option<SshSocket>,
- message_handlers: Mutex<
- HashMap<
- TypeId,
- Arc<
- dyn Send
- + Sync
- + Fn(
- Box<dyn AnyTypedEnvelope>,
- Arc<SshSession>,
- AsyncAppContext,
- ) -> Option<LocalBoxFuture<'static, Result<()>>>,
- >,
- >,
- >,
+ state: Mutex<ProtoMessageHandlerSet>,
}
struct SshClientState {
@@ -330,7 +314,7 @@ impl SshSession {
outgoing_tx,
spawn_process_tx,
client_socket,
- message_handlers: Default::default(),
+ state: Default::default(),
});
cx.spawn(|cx| {
@@ -351,18 +335,26 @@ impl SshSession {
} else if let Some(envelope) =
build_typed_envelope(peer_id, Instant::now(), incoming)
{
- log::debug!(
- "ssh message received. name:{}",
- envelope.payload_type_name()
- );
- let type_id = envelope.payload_type_id();
- let handler = this.message_handlers.lock().get(&type_id).cloned();
- if let Some(handler) = handler {
- if let Some(future) = handler(envelope, this.clone(), cx.clone()) {
- future.await.ok();
- } else {
- this.message_handlers.lock().remove(&type_id);
+ let type_name = envelope.payload_type_name();
+ if let Some(future) = ProtoMessageHandlerSet::handle_message(
+ &this.state,
+ envelope,
+ this.clone().into(),
+ cx.clone(),
+ ) {
+ log::debug!("ssh message received. name:{type_name}");
+ match future.await {
+ Ok(_) => {
+ log::debug!("ssh message handled. name:{type_name}");
+ }
+ Err(error) => {
+ log::error!(
+ "error handling message. type:{type_name}, error:{error:?}",
+ );
+ }
}
+ } else {
+ log::error!("unhandled ssh message name:{type_name}");
}
}
}
@@ -389,6 +381,7 @@ impl SshSession {
}
pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
+ log::debug!("ssh send name:{}", T::NAME);
self.send_dynamic(payload.into_envelope(0, None, None))
}
@@ -412,6 +405,22 @@ impl SshSession {
Ok(())
}
+ pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
+ let id = (TypeId::of::<E>(), remote_id);
+
+ let mut state = self.state.lock();
+ if state.entities_by_type_and_remote_id.contains_key(&id) {
+ panic!("already subscribed to entity");
+ }
+
+ state.entities_by_type_and_remote_id.insert(
+ id,
+ EntityMessageSubscriber::Entity {
+ handle: entity.downgrade().into(),
+ },
+ );
+ }
+
pub async fn spawn_process(&self, command: String) -> process::Child {
let (process_tx, process_rx) = oneshot::channel();
self.spawn_process_tx
@@ -426,54 +435,6 @@ impl SshSession {
pub fn ssh_args(&self) -> Vec<String> {
self.client_socket.as_ref().unwrap().ssh_args()
}
-
- pub fn add_message_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H)
- where
- M: EnvelopedMessage,
- E: 'static,
- H: 'static + Sync + Send + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
- F: 'static + Future<Output = Result<()>>,
- {
- let message_type_id = TypeId::of::<M>();
- self.message_handlers.lock().insert(
- message_type_id,
- Arc::new(move |envelope, _, cx| {
- let entity = entity.upgrade()?;
- let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
- Some(handler(entity, *envelope, cx).boxed_local())
- }),
- );
- }
-
- pub fn add_request_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H)
- where
- M: EnvelopedMessage + RequestMessage,
- E: 'static,
- H: 'static + Sync + Send + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
- F: 'static + Future<Output = Result<M::Response>>,
- {
- let message_type_id = TypeId::of::<M>();
- self.message_handlers.lock().insert(
- message_type_id,
- Arc::new(move |envelope, this, cx| {
- let entity = entity.upgrade()?;
- let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
- let request_id = envelope.message_id();
- Some(
- handler(entity, *envelope, cx)
- .then(move |result| async move {
- this.outgoing_tx.unbounded_send(result?.into_envelope(
- this.next_message_id.fetch_add(1, SeqCst),
- Some(request_id),
- None,
- ))?;
- Ok(())
- })
- .boxed_local(),
- )
- }),
- );
- }
}
impl ProtoClient for SshSession {
@@ -488,6 +449,14 @@ impl ProtoClient for SshSession {
fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
self.send_dynamic(envelope)
}
+
+ fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
+ self.send_dynamic(envelope)
+ }
+
+ fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
+ &self.state
+ }
}
impl SshClientState {
@@ -7,7 +7,7 @@ use project::{
};
use remote::SshSession;
use rpc::{
- proto::{self, AnyProtoClient, PeerId},
+ proto::{self, AnyProtoClient, SSH_PEER_ID, SSH_PROJECT_ID},
TypedEnvelope,
};
use settings::{Settings as _, SettingsStore};
@@ -18,9 +18,6 @@ use std::{
};
use worktree::Worktree;
-const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
-const PROJECT_ID: u64 = 0;
-
pub struct HeadlessProject {
pub fs: Arc<dyn Fs>,
pub session: AnyProtoClient,
@@ -36,48 +33,34 @@ impl HeadlessProject {
}
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
- let this = cx.weak_model();
-
let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone()));
let buffer_store = cx.new_model(|cx| {
- let mut buffer_store = BufferStore::new(worktree_store.clone(), Some(PROJECT_ID), cx);
- buffer_store.shared(PROJECT_ID, session.clone().into(), cx);
+ let mut buffer_store =
+ BufferStore::new(worktree_store.clone(), Some(SSH_PROJECT_ID), cx);
+ buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
buffer_store
});
- session.add_request_handler(this.clone(), Self::handle_list_remote_directory);
- session.add_request_handler(this.clone(), Self::handle_add_worktree);
- session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
- session.add_request_handler(this.clone(), Self::handle_find_search_candidates);
-
- session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_blame_buffer);
- session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_update_buffer);
- session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_save_buffer);
- session.add_message_handler(buffer_store.downgrade(), BufferStore::handle_close_buffer);
-
- session.add_request_handler(
- worktree_store.downgrade(),
- WorktreeStore::handle_create_project_entry,
- );
- session.add_request_handler(
- worktree_store.downgrade(),
- WorktreeStore::handle_rename_project_entry,
- );
- session.add_request_handler(
- worktree_store.downgrade(),
- WorktreeStore::handle_copy_project_entry,
- );
- session.add_request_handler(
- worktree_store.downgrade(),
- WorktreeStore::handle_delete_project_entry,
- );
- session.add_request_handler(
- worktree_store.downgrade(),
- WorktreeStore::handle_expand_project_entry,
- );
+ let client: AnyProtoClient = session.clone().into();
+
+ session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
+ session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
+ session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
+
+ client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
+
+ client.add_model_request_handler(Self::handle_add_worktree);
+ client.add_model_request_handler(Self::handle_open_buffer_by_path);
+ client.add_model_request_handler(Self::handle_find_search_candidates);
+
+ client.add_model_request_handler(BufferStore::handle_update_buffer);
+ client.add_model_message_handler(BufferStore::handle_close_buffer);
+
+ BufferStore::init(&client);
+ WorktreeStore::init(&client);
HeadlessProject {
- session: session.into(),
+ session: client,
fs,
worktree_store,
buffer_store,
@@ -144,7 +127,7 @@ impl HeadlessProject {
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
- .create_buffer_for_peer(&buffer, PEER_ID, cx)
+ .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
@@ -181,7 +164,7 @@ impl HeadlessProject {
response.buffer_ids.push(buffer_id.to_proto());
buffer_store
.update(&mut cx, |buffer_store, cx| {
- buffer_store.create_buffer_for_peer(&buffer, PEER_ID, cx)
+ buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
})?
.await?;
}
@@ -17,7 +17,7 @@ use smol::stream::StreamExt;
use std::{path::Path, sync::Arc};
#[gpui::test]
-async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
+async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (project, _headless, fs) = init_test(cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
@@ -639,14 +639,13 @@ impl Peer {
pub fn respond_with_unhandled_message(
&self,
- envelope: Box<dyn AnyTypedEnvelope>,
+ sender_id: ConnectionId,
+ request_message_id: u32,
+ message_type_name: &'static str,
) -> Result<()> {
- let connection = self.connection_state(envelope.sender_id().into())?;
+ let connection = self.connection_state(sender_id)?;
let response = ErrorCode::Internal
- .message(format!(
- "message {} was not handled",
- envelope.payload_type_name()
- ))
+ .message(format!("message {} was not handled", message_type_name))
.to_proto();
let message_id = connection
.next_message_id
@@ -655,7 +654,7 @@ impl Peer {
.outgoing_tx
.unbounded_send(proto::Message::Envelope(response.into_envelope(
message_id,
- Some(envelope.message_id()),
+ Some(request_message_id),
None,
)))?;
Ok(())