ssh remoting: Forward LSP logs to client (#19212)

Thorsten Ball and Bennet Bo Fenner created

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>

Change summary

crates/collab/src/db/queries/projects.rs     |   1 
crates/collab/src/db/queries/rooms.rs        |   1 
crates/language_tools/src/lsp_log.rs         | 192 ++++++++++++---------
crates/language_tools/src/lsp_log_tests.rs   |   3 
crates/project/src/lsp_store.rs              |  89 +++++++++
crates/project/src/project.rs                |   8 
crates/project/src/project_tests.rs          |  14 +
crates/proto/proto/zed.proto                 |  19 ++
crates/proto/src/proto.rs                    |   2 
crates/remote_server/Cargo.toml              |   1 
crates/remote_server/src/headless_project.rs |  10 +
11 files changed, 245 insertions(+), 95 deletions(-)

Detailed changes

crates/collab/src/db/queries/projects.rs 🔗

@@ -838,6 +838,7 @@ impl Database {
                 .map(|language_server| proto::LanguageServer {
                     id: language_server.id as u64,
                     name: language_server.name,
+                    worktree_id: None,
                 })
                 .collect(),
             dev_server_project_id: project.dev_server_project_id,

crates/collab/src/db/queries/rooms.rs 🔗

@@ -718,6 +718,7 @@ impl Database {
             .map(|language_server| proto::LanguageServer {
                 id: language_server.id as u64,
                 name: language_server.name,
+                worktree_id: None,
             })
             .collect::<Vec<_>>();
 

crates/language_tools/src/lsp_log.rs 🔗

@@ -11,7 +11,7 @@ use language::{LanguageServerId, LanguageServerName};
 use lsp::{
     notification::SetTrace, IoKind, LanguageServer, MessageType, SetTraceParams, TraceValue,
 };
-use project::{search::SearchQuery, Project};
+use project::{search::SearchQuery, Project, WorktreeId};
 use std::{borrow::Cow, sync::Arc};
 use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
 use workspace::{
@@ -99,6 +99,8 @@ impl Message for RpcMessage {
 }
 
 struct LanguageServerState {
+    name: Option<LanguageServerName>,
+    worktree_id: Option<WorktreeId>,
     kind: LanguageServerKind,
     log_messages: VecDeque<LogMessage>,
     trace_messages: VecDeque<TraceMessage>,
@@ -108,15 +110,34 @@ struct LanguageServerState {
     io_logs_subscription: Option<lsp::Subscription>,
 }
 
-enum LanguageServerKind {
+#[derive(PartialEq, Clone)]
+pub enum LanguageServerKind {
     Local { project: WeakModel<Project> },
-    Global { name: LanguageServerName },
+    Remote { project: WeakModel<Project> },
+    Global,
+}
+
+impl LanguageServerKind {
+    fn is_remote(&self) -> bool {
+        matches!(self, LanguageServerKind::Remote { .. })
+    }
+}
+
+impl std::fmt::Debug for LanguageServerKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
+            LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
+            LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
+        }
+    }
 }
 
 impl LanguageServerKind {
     fn project(&self) -> Option<&WeakModel<Project>> {
         match self {
             Self::Local { project } => Some(project),
+            Self::Remote { project } => Some(project),
             Self::Global { .. } => None,
         }
     }
@@ -175,6 +196,7 @@ pub(crate) struct LogMenuItem {
     pub rpc_trace_enabled: bool,
     pub selected_entry: LogKind,
     pub trace_level: lsp::TraceValue,
+    pub server_kind: LanguageServerKind,
 }
 
 actions!(debug, [OpenLanguageServerLogs]);
@@ -184,7 +206,7 @@ pub fn init(cx: &mut AppContext) {
 
     cx.observe_new_views(move |workspace: &mut Workspace, cx| {
         let project = workspace.project();
-        if project.read(cx).is_local() {
+        if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
             log_store.update(cx, |store, cx| {
                 store.add_project(project, cx);
             });
@@ -193,7 +215,7 @@ pub fn init(cx: &mut AppContext) {
         let log_store = log_store.clone();
         workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
             let project = workspace.project().read(cx);
-            if project.is_local() {
+            if project.is_local() || project.is_via_ssh() {
                 workspace.split_item(
                     SplitDirection::Right,
                     Box::new(cx.new_view(|cx| {
@@ -233,11 +255,12 @@ impl LogStore {
                                         .ok();
                                 },
                             ));
+                        let name = LanguageServerName::new_static("copilot");
                         this.add_language_server(
-                            LanguageServerKind::Global {
-                                name: LanguageServerName::new_static("copilot"),
-                            },
+                            LanguageServerKind::Global,
                             server.server_id(),
+                            Some(name),
+                            None,
                             Some(server.clone()),
                             cx,
                         );
@@ -279,42 +302,44 @@ impl LogStore {
                         this.language_servers
                             .retain(|_, state| state.kind.project() != Some(&weak_project));
                     }),
-                    cx.subscribe(project, |this, project, event, cx| match event {
-                        project::Event::LanguageServerAdded(id) => {
-                            let read_project = project.read(cx);
-                            if let Some(server) = read_project.language_server_for_id(*id, cx) {
+                    cx.subscribe(project, |this, project, event, cx| {
+                        let server_kind = if project.read(cx).is_via_ssh() {
+                            LanguageServerKind::Remote {
+                                project: project.downgrade(),
+                            }
+                        } else {
+                            LanguageServerKind::Local {
+                                project: project.downgrade(),
+                            }
+                        };
+
+                        match event {
+                            project::Event::LanguageServerAdded(id, name, worktree_id) => {
                                 this.add_language_server(
-                                    LanguageServerKind::Local {
-                                        project: project.downgrade(),
-                                    },
-                                    server.server_id(),
-                                    Some(server),
+                                    server_kind,
+                                    *id,
+                                    Some(name.clone()),
+                                    *worktree_id,
+                                    project.read(cx).language_server_for_id(*id, cx),
                                     cx,
                                 );
                             }
-                        }
-                        project::Event::LanguageServerRemoved(id) => {
-                            this.remove_language_server(*id, cx);
-                        }
-                        project::Event::LanguageServerLog(id, typ, message) => {
-                            this.add_language_server(
-                                LanguageServerKind::Local {
-                                    project: project.downgrade(),
-                                },
-                                *id,
-                                None,
-                                cx,
-                            );
-                            match typ {
-                                project::LanguageServerLogType::Log(typ) => {
-                                    this.add_language_server_log(*id, *typ, message, cx);
-                                }
-                                project::LanguageServerLogType::Trace(_) => {
-                                    this.add_language_server_trace(*id, message, cx);
+                            project::Event::LanguageServerRemoved(id) => {
+                                this.remove_language_server(*id, cx);
+                            }
+                            project::Event::LanguageServerLog(id, typ, message) => {
+                                this.add_language_server(server_kind, *id, None, None, None, cx);
+                                match typ {
+                                    project::LanguageServerLogType::Log(typ) => {
+                                        this.add_language_server_log(*id, *typ, message, cx);
+                                    }
+                                    project::LanguageServerLogType::Trace(_) => {
+                                        this.add_language_server_trace(*id, message, cx);
+                                    }
                                 }
                             }
+                            _ => {}
                         }
-                        _ => {}
                     }),
                 ],
             },
@@ -332,12 +357,16 @@ impl LogStore {
         &mut self,
         kind: LanguageServerKind,
         server_id: LanguageServerId,
+        name: Option<LanguageServerName>,
+        worktree_id: Option<WorktreeId>,
         server: Option<Arc<LanguageServer>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<&mut LanguageServerState> {
         let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
             cx.notify();
             LanguageServerState {
+                name: None,
+                worktree_id: None,
                 kind,
                 rpc_state: None,
                 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
@@ -348,6 +377,13 @@ impl LogStore {
             }
         });
 
+        if let Some(name) = name {
+            server_state.name = Some(name);
+        }
+        if let Some(worktree_id) = worktree_id {
+            server_state.worktree_id = Some(worktree_id);
+        }
+
         if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
             let io_tx = self.io_tx.clone();
             let server_id = server.server_id();
@@ -448,14 +484,14 @@ impl LogStore {
         self.language_servers
             .iter()
             .filter_map(move |(id, state)| match &state.kind {
-                LanguageServerKind::Local { project } => {
+                LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
                     if project == lookup_project {
                         Some(*id)
                     } else {
                         None
                     }
                 }
-                LanguageServerKind::Global { .. } => Some(*id),
+                LanguageServerKind::Global => Some(*id),
             })
     }
 
@@ -662,21 +698,40 @@ impl LspLogView {
     pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
         let log_store = self.log_store.read(cx);
 
-        let mut rows = self
-            .project
-            .read(cx)
-            .language_servers(cx)
-            .filter_map(|(server_id, language_server_name, worktree_id)| {
-                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
-                let state = log_store.language_servers.get(&server_id)?;
-                Some(LogMenuItem {
-                    server_id,
-                    server_name: language_server_name,
-                    worktree_root_name: worktree.read(cx).root_name().to_string(),
+        let unknown_server = LanguageServerName::new_static("unknown server");
+
+        let mut rows = log_store
+            .language_servers
+            .iter()
+            .filter_map(|(server_id, state)| match &state.kind {
+                LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
+                    let worktree_root_name = state
+                        .worktree_id
+                        .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
+                        .map(|worktree| worktree.read(cx).root_name().to_string())
+                        .unwrap_or_else(|| "Unknown worktree".to_string());
+
+                    let state = log_store.language_servers.get(&server_id)?;
+                    Some(LogMenuItem {
+                        server_id: *server_id,
+                        server_name: state.name.clone().unwrap_or(unknown_server.clone()),
+                        server_kind: state.kind.clone(),
+                        worktree_root_name,
+                        rpc_trace_enabled: state.rpc_state.is_some(),
+                        selected_entry: self.active_entry_kind,
+                        trace_level: lsp::TraceValue::Off,
+                    })
+                }
+
+                LanguageServerKind::Global => Some(LogMenuItem {
+                    server_id: *server_id,
+                    server_name: state.name.clone().unwrap_or(unknown_server.clone()),
+                    server_kind: state.kind.clone(),
+                    worktree_root_name: "supplementary".to_string(),
                     rpc_trace_enabled: state.rpc_state.is_some(),
                     selected_entry: self.active_entry_kind,
                     trace_level: lsp::TraceValue::Off,
-                })
+                }),
             })
             .chain(
                 self.project
@@ -687,6 +742,7 @@ impl LspLogView {
                         Some(LogMenuItem {
                             server_id,
                             server_name: name.clone(),
+                            server_kind: state.kind.clone(),
                             worktree_root_name: "supplementary".to_string(),
                             rpc_trace_enabled: state.rpc_state.is_some(),
                             selected_entry: self.active_entry_kind,
@@ -694,22 +750,6 @@ impl LspLogView {
                         })
                     }),
             )
-            .chain(
-                log_store
-                    .language_servers
-                    .iter()
-                    .filter_map(|(server_id, state)| match &state.kind {
-                        LanguageServerKind::Global { name } => Some(LogMenuItem {
-                            server_id: *server_id,
-                            server_name: name.clone(),
-                            worktree_root_name: "supplementary".to_string(),
-                            rpc_trace_enabled: state.rpc_state.is_some(),
-                            selected_entry: self.active_entry_kind,
-                            trace_level: lsp::TraceValue::Off,
-                        }),
-                        _ => None,
-                    }),
-            )
             .collect::<Vec<_>>();
         rows.sort_by_key(|row| row.server_id);
         rows.dedup_by_key(|row| row.server_id);
@@ -1075,13 +1115,9 @@ impl Render for LspLogToolbarItemView {
                                         view.show_logs_for_server(row.server_id, cx);
                                     }),
                                 );
-                            if server_selected && row.selected_entry == LogKind::Logs {
-                                let selected_ix = menu.select_last();
-                                debug_assert_eq!(
-                                    Some(ix * 4 + 1),
-                                    selected_ix,
-                                    "Could not scroll to a just added LSP menu item"
-                                );
+                            // We do not support tracing for remote language servers right now
+                            if row.server_kind.is_remote() {
+                                return menu;
                             }
                             menu = menu.entry(
                                 SERVER_TRACE,
@@ -1090,14 +1126,6 @@ impl Render for LspLogToolbarItemView {
                                     view.show_trace_for_server(row.server_id, cx);
                                 }),
                             );
-                            if server_selected && row.selected_entry == LogKind::Trace {
-                                let selected_ix = menu.select_last();
-                                debug_assert_eq!(
-                                    Some(ix * 4 + 2),
-                                    selected_ix,
-                                    "Could not scroll to a just added LSP menu item"
-                                );
-                            }
                             menu = menu.custom_entry(
                                 {
                                     let log_toolbar_view = log_toolbar_view.clone();

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -95,6 +95,9 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
                 rpc_trace_enabled: false,
                 selected_entry: LogKind::Logs,
                 trace_level: lsp::TraceValue::Off,
+                server_kind: lsp_log::LanguageServerKind::Local {
+                    project: project.downgrade()
+                }
             }]
         );
         assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");

crates/project/src/lsp_store.rs 🔗

@@ -696,7 +696,7 @@ pub struct LspStore {
 }
 
 pub enum LspStoreEvent {
-    LanguageServerAdded(LanguageServerId),
+    LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
     LanguageServerRemoved(LanguageServerId),
     LanguageServerUpdate {
         language_server_id: LanguageServerId,
@@ -752,6 +752,7 @@ impl LspStore {
         client.add_model_request_handler(Self::handle_restart_language_servers);
         client.add_model_message_handler(Self::handle_start_language_server);
         client.add_model_message_handler(Self::handle_update_language_server);
+        client.add_model_message_handler(Self::handle_language_server_log);
         client.add_model_message_handler(Self::handle_update_diagnostic_summary);
         client.add_model_request_handler(Self::handle_format_buffers);
         client.add_model_request_handler(Self::handle_resolve_completion_documentation);
@@ -3087,6 +3088,7 @@ impl LspStore {
                     server: Some(proto::LanguageServer {
                         id: server_id.0 as u64,
                         name: status.name.clone(),
+                        worktree_id: None,
                     }),
                 })
                 .log_err();
@@ -3907,16 +3909,23 @@ impl LspStore {
             .payload
             .server
             .ok_or_else(|| anyhow!("invalid server"))?;
+
         this.update(&mut cx, |this, cx| {
+            let server_id = LanguageServerId(server.id as usize);
             this.language_server_statuses.insert(
-                LanguageServerId(server.id as usize),
+                server_id,
                 LanguageServerStatus {
-                    name: server.name,
+                    name: server.name.clone(),
                     pending_work: Default::default(),
                     has_pending_diagnostic_updates: false,
                     progress_tokens: Default::default(),
                 },
             );
+            cx.emit(LspStoreEvent::LanguageServerAdded(
+                server_id,
+                LanguageServerName(server.name.into()),
+                server.worktree_id.map(WorktreeId::from_proto),
+            ));
             cx.notify();
         })?;
         Ok(())
@@ -3984,6 +3993,29 @@ impl LspStore {
         })?
     }
 
+    async fn handle_language_server_log(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::LanguageServerLog>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize);
+        let log_type = envelope
+            .payload
+            .log_type
+            .map(LanguageServerLogType::from_proto)
+            .context("invalid language server log type")?;
+
+        let message = envelope.payload.message;
+
+        this.update(&mut cx, |_, cx| {
+            cx.emit(LspStoreEvent::LanguageServerLog(
+                language_server_id,
+                log_type,
+                message,
+            ));
+        })
+    }
+
     pub fn disk_based_diagnostics_started(
         &mut self,
         language_server_id: LanguageServerId,
@@ -6356,7 +6388,11 @@ impl LspStore {
             },
         );
 
-        cx.emit(LspStoreEvent::LanguageServerAdded(server_id));
+        cx.emit(LspStoreEvent::LanguageServerAdded(
+            server_id,
+            language_server.name().into(),
+            Some(key.0),
+        ));
 
         if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() {
             downstream_client
@@ -6365,6 +6401,7 @@ impl LspStore {
                     server: Some(proto::LanguageServer {
                         id: server_id.0 as u64,
                         name: language_server.name().to_string(),
+                        worktree_id: Some(key.0.to_proto()),
                     }),
                 })
                 .log_err();
@@ -6546,8 +6583,8 @@ impl LspStore {
         if let Some(local) = self.as_local_mut() {
             local
                 .supplementary_language_servers
-                .insert(id, (name, server));
-            cx.emit(LspStoreEvent::LanguageServerAdded(id));
+                .insert(id, (name.clone(), server));
+            cx.emit(LspStoreEvent::LanguageServerAdded(id, name, None));
         }
     }
 
@@ -7289,6 +7326,46 @@ pub enum LanguageServerLogType {
     Trace(Option<String>),
 }
 
+impl LanguageServerLogType {
+    pub fn to_proto(&self) -> proto::language_server_log::LogType {
+        match self {
+            Self::Log(log_type) => {
+                let message_type = match *log_type {
+                    MessageType::ERROR => 1,
+                    MessageType::WARNING => 2,
+                    MessageType::INFO => 3,
+                    MessageType::LOG => 4,
+                    other => {
+                        log::warn!("Unknown lsp log message type: {:?}", other);
+                        4
+                    }
+                };
+                proto::language_server_log::LogType::LogMessageType(message_type)
+            }
+            Self::Trace(message) => {
+                proto::language_server_log::LogType::LogTrace(proto::LspLogTrace {
+                    message: message.clone(),
+                })
+            }
+        }
+    }
+
+    pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self {
+        match log_type {
+            proto::language_server_log::LogType::LogMessageType(message_type) => {
+                Self::Log(match message_type {
+                    1 => MessageType::ERROR,
+                    2 => MessageType::WARNING,
+                    3 => MessageType::INFO,
+                    4 => MessageType::LOG,
+                    _ => MessageType::LOG,
+                })
+            }
+            proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message),
+        }
+    }
+}
+
 pub enum LanguageServerState {
     Starting(Task<Option<Arc<LanguageServer>>>),
 

crates/project/src/project.rs 🔗

@@ -219,7 +219,7 @@ enum ProjectClientState {
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum Event {
-    LanguageServerAdded(LanguageServerId),
+    LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
     LanguageServerRemoved(LanguageServerId),
     LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
     Notification(String),
@@ -2090,9 +2090,9 @@ impl Project {
                 path: path.clone(),
                 language_server_id: *language_server_id,
             }),
-            LspStoreEvent::LanguageServerAdded(language_server_id) => {
-                cx.emit(Event::LanguageServerAdded(*language_server_id))
-            }
+            LspStoreEvent::LanguageServerAdded(language_server_id, name, worktree_id) => cx.emit(
+                Event::LanguageServerAdded(*language_server_id, name.clone(), *worktree_id),
+            ),
             LspStoreEvent::LanguageServerRemoved(language_server_id) => {
                 cx.emit(Event::LanguageServerRemoved(*language_server_id))
             }

crates/project/src/project_tests.rs 🔗

@@ -1185,7 +1185,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
     assert_eq!(
         events.next().await.unwrap(),
-        Event::LanguageServerAdded(LanguageServerId(0)),
+        Event::LanguageServerAdded(
+            LanguageServerId(0),
+            fake_server.server.name().into(),
+            Some(worktree_id)
+        ),
     );
 
     fake_server
@@ -1295,6 +1299,8 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
         },
     );
 
+    let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
+
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
         .await
@@ -1314,7 +1320,11 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
     let fake_server = fake_servers.next().await.unwrap();
     assert_eq!(
         events.next().await.unwrap(),
-        Event::LanguageServerAdded(LanguageServerId(1))
+        Event::LanguageServerAdded(
+            LanguageServerId(1),
+            fake_server.server.name().into(),
+            Some(worktree_id)
+        )
     );
     fake_server.start_progress(progress_token).await;
     assert_eq!(

crates/proto/proto/zed.proto 🔗

@@ -269,7 +269,7 @@ message Envelope {
 
         GetLlmToken get_llm_token = 235;
         GetLlmTokenResponse get_llm_token_response = 236;
-        RefreshLlmToken refresh_llm_token = 259; // current max
+        RefreshLlmToken refresh_llm_token = 259;
 
         LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241;
         LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242;
@@ -286,6 +286,8 @@ message Envelope {
         ShutdownRemoteServer shutdown_remote_server = 257;
 
         RemoveWorktree remove_worktree = 258;
+
+        LanguageServerLog language_server_log = 260; // current max
     }
 
     reserved 87 to 88;
@@ -1294,6 +1296,7 @@ message LamportTimestamp {
 message LanguageServer {
     uint64 id = 1;
     string name = 2;
+    optional uint64 worktree_id = 3;
 }
 
 message StartLanguageServer {
@@ -1347,6 +1350,20 @@ message LspDiskBasedDiagnosticsUpdating {}
 
 message LspDiskBasedDiagnosticsUpdated {}
 
+message LanguageServerLog {
+    uint64 project_id = 1;
+    uint64 language_server_id = 2;
+    oneof log_type {
+        uint32 log_message_type = 3;
+        LspLogTrace log_trace = 4;
+    }
+    string message = 5;
+}
+
+message LspLogTrace {
+    optional string message = 1;
+}
+
 message UpdateChannels {
     repeated Channel channels = 1;
     repeated uint64 delete_channels = 4;

crates/proto/src/proto.rs 🔗

@@ -366,6 +366,7 @@ messages!(
     (CheckFileExistsResponse, Background),
     (ShutdownRemoteServer, Foreground),
     (RemoveWorktree, Foreground),
+    (LanguageServerLog, Foreground),
 );
 
 request_messages!(
@@ -562,6 +563,7 @@ entity_messages!(
     LspExtSwitchSourceHeader,
     UpdateUserSettings,
     CheckFileExists,
+    LanguageServerLog,
 );
 
 entity_messages!(

crates/remote_server/Cargo.toml 🔗

@@ -33,6 +33,7 @@ gpui.workspace = true
 language.workspace = true
 languages.workspace = true
 log.workspace = true
+lsp.workspace = true
 node_runtime.workspace = true
 project.workspace = true
 remote.workspace = true

crates/remote_server/src/headless_project.rs 🔗

@@ -203,6 +203,16 @@ impl HeadlessProject {
                     })
                     .log_err();
             }
+            LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
+                self.session
+                    .send(proto::LanguageServerLog {
+                        project_id: SSH_PROJECT_ID,
+                        language_server_id: language_server_id.to_proto(),
+                        message: message.clone(),
+                        log_type: Some(log_type.to_proto()),
+                    })
+                    .log_err();
+            }
             _ => {}
         }
     }