From 397e4bee0a9ab26ccfb36596dc6440ae3a31f0e8 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 15 Oct 2024 16:04:29 +0200 Subject: [PATCH] ssh remoting: Forward LSP logs to client (#19212) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- 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(-) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index ceac78203d..f4eabf4979 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/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, diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index baba0f2cf9..a0bb9fed69 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/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::>(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index aee39ff0a0..9f6eb62817 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/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, + worktree_id: Option, kind: LanguageServerKind, log_messages: VecDeque, trace_messages: VecDeque, @@ -108,15 +110,34 @@ struct LanguageServerState { io_logs_subscription: Option, } -enum LanguageServerKind { +#[derive(PartialEq, Clone)] +pub enum LanguageServerKind { Local { project: WeakModel }, - Global { name: LanguageServerName }, + Remote { project: WeakModel }, + 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> { 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, + worktree_id: Option, server: Option>, cx: &mut ModelContext, ) -> 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> { 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::>(); 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(); diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 3ead695434..6b45cfc8dc 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/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"); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 01e40ec6a0..58d6b07e56 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -696,7 +696,7 @@ pub struct LspStore { } pub enum LspStoreEvent { - LanguageServerAdded(LanguageServerId), + LanguageServerAdded(LanguageServerId, LanguageServerName, Option), 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, + envelope: TypedEnvelope, + 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), } +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>>), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2692c711bf..f4040a3d12 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -219,7 +219,7 @@ enum ProjectClientState { #[derive(Clone, Debug, PartialEq)] pub enum Event { - LanguageServerAdded(LanguageServerId), + LanguageServerAdded(LanguageServerId, LanguageServerName, Option), 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)) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index f5cfba2ffd..17eeae1c3d 100644 --- a/crates/project/src/project_tests.rs +++ b/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!( diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f6711f8db9..52a73cd7f1 100644 --- a/crates/proto/proto/zed.proto +++ b/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; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 2c038c2e1c..15841a7077 100644 --- a/crates/proto/src/proto.rs +++ b/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!( diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 25c5860a00..d822721c1e 100644 --- a/crates/remote_server/Cargo.toml +++ b/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 diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 96d769a50c..701b80842b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/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(); + } _ => {} } }