diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..c68c2eae3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9213,6 +9213,7 @@ dependencies = [ "language", "lsp", "project", + "proto", "release_channel", "serde_json", "settings", @@ -13500,6 +13501,7 @@ dependencies = [ "language", "language_extension", "language_model", + "language_tools", "languages", "libc", "log", diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 5aa914311a..b8f85d8d90 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,6 +24,7 @@ itertools.workspace = true language.workspace = true lsp.workspace = true project.workspace = true +proto.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index cbf5756875..34c3d4b9eb 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -1,5 +1,5 @@ mod key_context_view; -mod lsp_log; +pub mod lsp_log; pub mod lsp_tool; mod syntax_tree_view; diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..2b1c82cd1f 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -12,9 +12,10 @@ use lsp::{ IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, SetTraceParams, TraceValue, notification::SetTrace, }; -use project::{Project, WorktreeId, search::SearchQuery}; +use project::{Project, WorktreeId, lsp_store::LanguageServerLogType, search::SearchQuery}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; +use util::ResultExt as _; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, @@ -36,7 +37,7 @@ pub struct LogStore { } struct ProjectState { - _subscriptions: [gpui::Subscription; 2], + _subscriptions: [gpui::Subscription; 3], } trait Message: AsRef { @@ -102,6 +103,7 @@ impl Message for RpcMessage { } pub(super) struct LanguageServerState { + project: WeakEntity, name: Option, worktree_id: Option, kind: LanguageServerKind, @@ -183,6 +185,13 @@ pub enum LogKind { } impl LogKind { + fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { + match log_type { + LanguageServerLogType::Log(_) => Self::Logs, + LanguageServerLogType::Trace(_) => Self::Trace, + LanguageServerLogType::Rpc { .. } => Self::Rpc, + } + } fn label(&self) -> &'static str { match self { LogKind::Rpc => RPC_MESSAGES, @@ -212,10 +221,11 @@ actions!( ] ); -pub(super) struct GlobalLogStore(pub WeakEntity); +pub struct GlobalLogStore(pub WeakEntity); impl Global for GlobalLogStore {} +// todo! do separate headless and local cases here: headless cares only about the downstream_client() part, NO log storage is needed pub fn init(cx: &mut App) { let log_store = cx.new(LogStore::new); cx.set_global(GlobalLogStore(log_store.downgrade())); @@ -311,6 +321,7 @@ impl LogStore { pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { let weak_project = project.downgrade(); + let subscription_weak_project = weak_project.clone(); self.projects.insert( project.downgrade(), ProjectState { @@ -356,13 +367,42 @@ impl LogStore { this.add_language_server_log(*id, *typ, message, cx); } project::LanguageServerLogType::Trace(_) => { + // todo! do something with trace level this.add_language_server_trace(*id, message, cx); } + project::LanguageServerLogType::Rpc { received } => { + let kind = if *received { + MessageKind::Receive + } else { + MessageKind::Send + }; + this.add_language_server_rpc(*id, kind, message, cx); + } } } _ => {} } }), + cx.subscribe_self(move |_, e, cx| match e { + Event::NewServerLogEntry { id, kind, text } => { + subscription_weak_project + .update(cx, |project, cx| { + if let Some((client, project_id)) = + project.lsp_store().read(cx).downstream_client() + { + client + .send(proto::LanguageServerLog { + project_id, + language_server_id: id.to_proto(), + message: text.clone(), + log_type: Some(kind.to_proto()), + }) + .log_err(); + }; + }) + .ok(); + } + }), ], }, ); @@ -382,6 +422,7 @@ impl LogStore { name: Option, worktree_id: Option, server: Option>, + project: WeakEntity, cx: &mut Context, ) -> Option<&mut LanguageServerState> { let server_state = self.language_servers.entry(server_id).or_insert_with(|| { @@ -390,6 +431,7 @@ impl LogStore { name: None, worktree_id: None, kind, + project, rpc_state: None, log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), @@ -429,17 +471,21 @@ impl LogStore { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; - Self::add_language_server_message( + if let Some(new_message) = Self::push_new_message( log_lines, - id, LogMessage { message: message.trim_end().to_string(), typ, }, language_server_state.log_level, - LogKind::Logs, - cx, - ); + ) { + cx.emit(Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Log(typ), + text: new_message, + }); + } + Some(()) } @@ -452,38 +498,81 @@ impl LogStore { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; - Self::add_language_server_message( + if let Some(new_message) = Self::push_new_message( log_lines, - id, TraceMessage { message: message.trim().to_string(), }, (), - LogKind::Trace, - cx, - ); + ) { + cx.emit(Event::NewServerLogEntry { + id, + // todo! Ben, fix this here too! + kind: LanguageServerLogType::Trace(project::lsp_store::TraceLevel::Verbose), + text: new_message, + }); + } + Some(()) } - fn add_language_server_message( + fn push_new_message( log_lines: &mut VecDeque, - id: LanguageServerId, message: T, current_severity: ::Level, - kind: LogKind, - cx: &mut Context, - ) { + ) -> Option { while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } - let text = message.as_ref().to_string(); let visible = message.should_include(current_severity); - log_lines.push_back(message); - if visible { - cx.emit(Event::NewServerLogEntry { id, kind, text }); - cx.notify(); + let re = visible.then(|| message.as_ref().to_string()); + log_lines.push_back(message); + re + } + + fn add_language_server_rpc( + &mut self, + language_server_id: LanguageServerId, + kind: MessageKind, + message: &str, + cx: &mut Context<'_, LogStore>, + ) { + let Some(state) = self + .get_language_server_state(language_server_id) + .and_then(|state| state.rpc_state.as_mut()) + else { + return; + }; + + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + rpc_log_lines.push_back(RpcMessage { + message: line_before_message.to_string(), + }); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + kind: LanguageServerLogType::Rpc { + received: kind == MessageKind::Receive, + }, + text: line_before_message.to_string(), + }); } + + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + + rpc_log_lines.push_back(RpcMessage { + message: message.trim().to_owned(), + }); } fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { @@ -520,7 +609,7 @@ impl LogStore { }) } - fn enable_rpc_trace_for_language_server( + pub fn enable_rpc_trace_for_language_server( &mut self, server_id: LanguageServerId, ) -> Option<&mut LanguageServerRpcState> { @@ -663,47 +752,19 @@ impl LogStore { } }; - let state = self - .get_language_server_state(language_server_id)? - .rpc_state - .as_mut()?; let kind = if is_received { MessageKind::Receive } else { MessageKind::Send }; - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LogKind::Rpc, - text: line_before_message.to_string(), - }); - } - - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - - let message = message.trim(); - rpc_log_lines.push_back(RpcMessage { - message: message.to_string(), - }); + self.add_language_server_rpc(language_server_id, kind, message, cx); cx.emit(Event::NewServerLogEntry { id: language_server_id, - kind: LogKind::Rpc, - text: message.to_string(), + kind: LanguageServerLogType::Rpc { + received: is_received, + }, + text: message.to_owned(), }); cx.notify(); Some(()) @@ -757,7 +818,7 @@ impl LspLogView { move |log_view, _, e, window, cx| match e { Event::NewServerLogEntry { id, kind, text } => { if log_view.current_server_id == Some(*id) - && *kind == log_view.active_entry_kind + && LogKind::from_server_log_type(kind) == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -1075,6 +1136,21 @@ impl LspLogView { } else { log_store.disable_rpc_trace_for_language_server(server_id); } + + if let Some(server_state) = log_store.language_servers.get(server_id) { + server_state + .project + .update(cx, |project, cx| { + if let Some((client, project)) = + project.lsp_store().read(cx).upstream_client() + { + // todo! client.send a new proto message to propagate the enabled + // !!!! we have to have a handler on both headless and normal projects + // that handler has to touch the Global and amend the sending bit + } + }) + .ok(); + }; }); if !enabled && Some(server_id) == self.current_server_id { self.show_logs_for_server(server_id, window, cx); @@ -1113,6 +1189,8 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { + // todo! there's no language server for the remote case, hence no server info! + // BUT we do have the capabilities info within the LspStore.lsp_server_capabilities let lsp_store = self.project.read(cx).lsp_store(); let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else { return; @@ -1737,7 +1815,7 @@ impl LspLogToolbarItemView { pub enum Event { NewServerLogEntry { id: LanguageServerId, - kind: LogKind, + kind: LanguageServerLogType, text: String, }, } diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index dd3e80212f..99128e72d9 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -122,8 +122,7 @@ impl LanguageServerState { let lsp_logs = cx .try_global::() .and_then(|lsp_logs| lsp_logs.0.upgrade()); - let lsp_store = self.lsp_store.upgrade(); - let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else { + let Some(lsp_logs) = lsp_logs else { return menu; }; @@ -210,10 +209,7 @@ impl LanguageServerState { }; let server_selector = server_info.server_selector(); - // TODO currently, Zed remote does not work well with the LSP logs - // https://github.com/zed-industries/zed/issues/28557 - let has_logs = lsp_store.read(cx).as_local().is_some() - && lsp_logs.read(cx).has_server_logs(&server_selector); + let has_logs = lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..5c2782b3b9 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -977,7 +977,13 @@ impl LocalLspStore { this.update(&mut cx, |_, cx| { cx.emit(LspStoreEvent::LanguageServerLog( server_id, - LanguageServerLogType::Trace(params.verbose), + // todo! store verbose info on Verbose + LanguageServerLogType::Trace( + params + .verbose + .map(|_verbose_info| TraceLevel::Verbose) + .unwrap_or(TraceLevel::Messages), + ), params.message, )); }) @@ -12168,6 +12174,10 @@ impl LspStore { let data = self.lsp_code_lens.get_mut(&buffer_id)?; Some(data.update.take()?.1) } + + pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { + self.downstream_client.clone() + } } // Registration with registerOptions as null, should fallback to true. @@ -12674,48 +12684,92 @@ impl PartialEq for LanguageServerPromptRequest { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TraceLevel { + Off, + Messages, + Verbose, +} + #[derive(Clone, Debug, PartialEq)] pub enum LanguageServerLogType { Log(MessageType), - Trace(Option), + Trace(TraceLevel), + Rpc { received: bool }, } 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, + use proto::log_message::LogLevel; + let level = match *log_type { + MessageType::ERROR => LogLevel::Error, + MessageType::WARNING => LogLevel::Warning, + MessageType::INFO => LogLevel::Info, + MessageType::LOG => LogLevel::Log, other => { - log::warn!("Unknown lsp log message type: {:?}", other); - 4 + log::warn!("Unknown lsp log message type: {other:?}"); + LogLevel::Log } }; - proto::language_server_log::LogType::LogMessageType(message_type) - } - Self::Trace(message) => { - proto::language_server_log::LogType::LogTrace(proto::LspLogTrace { - message: message.clone(), + proto::language_server_log::LogType::Log(proto::LogMessage { + level: level as i32, }) } + Self::Trace(trace_level) => { + use proto::trace_message; + let level = match trace_level { + TraceLevel::Off => trace_message::TraceLevel::Off, + TraceLevel::Messages => trace_message::TraceLevel::Messages, + TraceLevel::Verbose => trace_message::TraceLevel::Verbose, + }; + proto::language_server_log::LogType::Trace(proto::TraceMessage { + level: level as i32, + }) + } + Self::Rpc { received } => { + let kind = if *received { + proto::rpc_message::Kind::Received + } else { + proto::rpc_message::Kind::Sent + }; + let kind = kind as i32; + proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) + } } } pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { + use proto::log_message::LogLevel; + use proto::rpc_message; + use proto::trace_message; 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), + proto::language_server_log::LogType::Log(message_type) => Self::Log( + match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { + LogLevel::Error => MessageType::ERROR, + LogLevel::Warning => MessageType::WARNING, + LogLevel::Info => MessageType::INFO, + LogLevel::Log => MessageType::LOG, + }, + ), + proto::language_server_log::LogType::Trace(trace) => Self::Trace( + match trace_message::TraceLevel::from_i32(trace.level) + .unwrap_or(trace_message::TraceLevel::Messages) + { + trace_message::TraceLevel::Off => TraceLevel::Off, + trace_message::TraceLevel::Messages => TraceLevel::Messages, + trace_message::TraceLevel::Verbose => TraceLevel::Verbose, + }, + ), + proto::language_server_log::LogType::Rpc(message) => Self::Rpc { + received: match rpc_message::Kind::from_i32(message.kind) + .unwrap_or(rpc_message::Kind::Received) + { + rpc_message::Kind::Received => true, + rpc_message::Kind::Sent => false, + }, + }, } } } diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38c..f935f2263b 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -610,11 +610,42 @@ message ServerMetadataUpdated { message LanguageServerLog { uint64 project_id = 1; uint64 language_server_id = 2; + string message = 3; oneof log_type { - uint32 log_message_type = 3; - LspLogTrace log_trace = 4; + LogMessage log = 4; + TraceMessage trace = 5; + RpcMessage rpc = 6; + } +} + +message LogMessage { + LogLevel level = 1; + + enum LogLevel { + LOG = 0; + INFO = 1; + WARNING = 2; + ERROR = 3; + } +} + +message TraceMessage { + TraceLevel level = 1; + + enum TraceLevel { + OFF = 0; + MESSAGES = 1; + VERBOSE = 2; + } +} + +message RpcMessage { + Kind kind = 1; + + enum Kind { + RECEIVED = 0; + SENT = 1; } - string message = 5; } message LspLogTrace { diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..249968b246 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -43,6 +43,7 @@ gpui_tokio.workspace = true http_client.workspace = true language.workspace = true language_extension.workspace = true +language_tools.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6216ff7728..14e2fbce21 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -65,6 +65,13 @@ impl HeadlessProject { settings::init(cx); language::init(cx); project::Project::init_settings(cx); + // todo! what to do with the RPC log spam? + // if we have not enabled RPC logging on the remote client, we do not need these + // + // Maybe, add another RPC message, proto::ToggleRpcLogging(bool) + // and send it into the upstream client from the remotes, so that the local/headless counterpart + // can access this Global and toggle the spam send + language_tools::lsp_log::init(cx); } pub fn new(