diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 0fabdab660..e2fabdc7b5 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -8,7 +8,7 @@ use gpui::{ ViewContext, VisualContext, WeakModel, WindowContext, }; use language::{LanguageServerId, LanguageServerName}; -use lsp::{IoKind, LanguageServer}; +use lsp::{notification::SetTrace, IoKind, LanguageServer, SetTraceParams, TraceValue}; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection}; @@ -37,9 +37,12 @@ struct ProjectState { struct LanguageServerState { kind: LanguageServerKind, log_messages: VecDeque, + trace_messages: VecDeque, rpc_state: Option, + trace_level: TraceValue, _io_logs_subscription: Option, _lsp_logs_subscription: Option, + _lsp_trace_subscription: Option, } enum LanguageServerKind { @@ -66,7 +69,7 @@ pub struct LspLogView { editor_subscriptions: Vec, log_store: Model, current_server_id: Option, - is_showing_rpc_trace: bool, + active_entry_kind: LogKind, project: Model, focus_handle: FocusHandle, _log_store_subscriptions: Vec, @@ -83,14 +86,32 @@ enum MessageKind { Receive, } +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum LogKind { + Rpc, + Trace, + #[default] + Logs, +} + +impl LogKind { + fn label(&self) -> &'static str { + match self { + LogKind::Rpc => RPC_MESSAGES, + LogKind::Trace => SERVER_TRACE, + LogKind::Logs => SERVER_LOGS, + } + } +} + #[derive(Clone, Debug, PartialEq)] pub(crate) struct LogMenuItem { pub server_id: LanguageServerId, pub server_name: LanguageServerName, pub worktree_root_name: String, pub rpc_trace_enabled: bool, - pub rpc_trace_selected: bool, - pub logs_selected: bool, + pub selected_entry: LogKind, + pub trace_level: lsp::TraceValue, } actions!(debug, [OpenLanguageServerLogs]); @@ -244,12 +265,17 @@ impl LogStore { kind, rpc_state: None, log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_level: TraceValue::Off, _io_logs_subscription: None, _lsp_logs_subscription: None, + _lsp_trace_subscription: None, } }); - if server.has_notification_handler::() { + if server.has_notification_handler::() + || server.has_notification_handler::() + { // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. return Some(server_state); } @@ -264,6 +290,7 @@ impl LogStore { let this = cx.handle().downgrade(); server_state._lsp_logs_subscription = Some(server.on_notification::({ + let this = this.clone(); move |params, mut cx| { if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { @@ -273,6 +300,17 @@ impl LogStore { } } })); + server_state._lsp_trace_subscription = + Some(server.on_notification::({ + move |params, mut cx| { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.add_language_server_trace(server_id, ¶ms.message, cx); + }) + .ok(); + } + } + })); Some(server_state) } @@ -285,6 +323,30 @@ 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(log_lines, id, message, LogKind::Logs, cx); + Some(()) + } + + fn add_language_server_trace( + &mut self, + id: LanguageServerId, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + let language_server_state = self.get_language_server_state(id)?; + + let log_lines = &mut language_server_state.trace_messages; + Self::add_language_server_message(log_lines, id, message, LogKind::Trace, cx); + Some(()) + } + + fn add_language_server_message( + log_lines: &mut VecDeque, + id: LanguageServerId, + message: &str, + kind: LogKind, + cx: &mut ModelContext, + ) { while log_lines.len() >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } @@ -293,10 +355,9 @@ impl LogStore { cx.emit(Event::NewServerLogEntry { id, entry: message.to_string(), - is_rpc: false, + kind, }); cx.notify(); - Some(()) } fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext) { @@ -307,7 +368,9 @@ impl LogStore { fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { Some(&self.language_servers.get(&server_id)?.log_messages) } - + fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { + Some(&self.language_servers.get(&server_id)?.trace_messages) + } fn server_ids_for_project<'a>( &'a self, lookup_project: &'a WeakModel, @@ -386,7 +449,7 @@ impl LogStore { cx.emit(Event::NewServerLogEntry { id: language_server_id, entry: line_before_message.to_string(), - is_rpc: true, + kind: LogKind::Rpc, }); } @@ -398,7 +461,7 @@ impl LogStore { cx.emit(Event::NewServerLogEntry { id: language_server_id, entry: message.to_string(), - is_rpc: true, + kind: LogKind::Rpc, }); cx.notify(); Some(()) @@ -425,10 +488,10 @@ impl LspLogView { if let Some(current_lsp) = this.current_server_id { if !store.read(cx).language_servers.contains_key(¤t_lsp) { if let Some(server_id) = first_server_id_for_project { - if this.is_showing_rpc_trace { - this.show_rpc_trace_for_server(server_id, cx) - } else { - this.show_logs_for_server(server_id, cx) + match this.active_entry_kind { + LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx), + LogKind::Trace => this.show_trace_for_server(server_id, cx), + LogKind::Logs => this.show_logs_for_server(server_id, cx), } } else { this.current_server_id = None; @@ -441,21 +504,19 @@ impl LspLogView { } } } else if let Some(server_id) = first_server_id_for_project { - if this.is_showing_rpc_trace { - this.show_rpc_trace_for_server(server_id, cx) - } else { - this.show_logs_for_server(server_id, cx) + match this.active_entry_kind { + LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx), + LogKind::Trace => this.show_trace_for_server(server_id, cx), + LogKind::Logs => this.show_logs_for_server(server_id, cx), } } cx.notify(); }); let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { - Event::NewServerLogEntry { id, entry, is_rpc } => { + Event::NewServerLogEntry { id, entry, kind } => { if log_view.current_server_id == Some(*id) { - if (*is_rpc && log_view.is_showing_rpc_trace) - || (!*is_rpc && !log_view.is_showing_rpc_trace) - { + if *kind == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); let last_point = editor.buffer().read(cx).len(cx); @@ -486,7 +547,7 @@ impl LspLogView { project, log_store, current_server_id: None, - is_showing_rpc_trace: false, + active_entry_kind: LogKind::Logs, _log_store_subscriptions: vec![ model_changes_subscription, events_subscriptions, @@ -541,10 +602,8 @@ impl LspLogView { server_name: language_server_name, worktree_root_name: worktree.read(cx).root_name().to_string(), rpc_trace_enabled: state.rpc_state.is_some(), - rpc_trace_selected: self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - logs_selected: !self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), + selected_entry: self.active_entry_kind, + trace_level: lsp::TraceValue::Off, }) }) .chain( @@ -558,10 +617,8 @@ impl LspLogView { server_name: name.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), - rpc_trace_selected: self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - logs_selected: !self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), + selected_entry: self.active_entry_kind, + trace_level: lsp::TraceValue::Off, }) }), ) @@ -575,10 +632,8 @@ impl LspLogView { server_name: name.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), - rpc_trace_selected: self.is_showing_rpc_trace - && self.current_server_id == Some(*server_id), - logs_selected: !self.is_showing_rpc_trace - && self.current_server_id == Some(*server_id), + selected_entry: self.active_entry_kind, + trace_level: lsp::TraceValue::Off, }), _ => None, }), @@ -597,7 +652,23 @@ impl LspLogView { .map(log_contents); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); - self.is_showing_rpc_trace = false; + self.active_entry_kind = LogKind::Logs; + let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx); + self.editor = editor; + self.editor_subscriptions = editor_subscriptions; + cx.notify(); + } + cx.focus(&self.focus_handle); + } + fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { + let log_contents = self + .log_store + .read(cx) + .server_trace(server_id) + .map(log_contents); + if let Some(log_contents) = log_contents { + self.current_server_id = Some(server_id); + self.active_entry_kind = LogKind::Trace; let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; @@ -618,7 +689,7 @@ impl LspLogView { }); if let Some(rpc_log) = rpc_log { self.current_server_id = Some(server_id); - self.is_showing_rpc_trace = true; + self.active_entry_kind = LogKind::Rpc; let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); editor @@ -666,6 +737,24 @@ impl LspLogView { cx.notify(); } } + fn update_trace_level( + &self, + server_id: LanguageServerId, + level: TraceValue, + cx: &mut ViewContext, + ) { + if let Some(server) = self.project.read(cx).language_server_for_id(server_id) { + self.log_store.update(cx, |this, _| { + if let Some(state) = this.get_language_server_state(server_id) { + state.trace_level = level; + } + }); + + server + .notify::(SetTraceParams { value: level }) + .ok(); + } + } } fn log_contents(lines: &VecDeque) -> String { @@ -826,123 +915,195 @@ impl Render for LspLogToolbarItemView { "{} ({}) - {}", row.server_name.0, row.worktree_root_name, - if row.rpc_trace_selected { - RPC_MESSAGES - } else { - SERVER_LOGS - }, + row.selected_entry.label() )) }) .unwrap_or_else(|| "No server selected".into()), )) - .menu(move |cx| { - let menu_rows = menu_rows.clone(); + .menu({ let log_view = log_view.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - ContextMenu::build(cx, move |mut menu, cx| { - for (ix, row) in menu_rows.into_iter().enumerate() { - let server_selected = Some(row.server_id) == current_server_id; - menu = menu - .header(format!( - "{} ({})", - row.server_name.0, row.worktree_root_name - )) - .entry( - SERVER_LOGS, + move |cx| { + let menu_rows = menu_rows.clone(); + let log_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for (ix, row) in menu_rows.into_iter().enumerate() { + let server_selected = Some(row.server_id) == current_server_id; + menu = menu + .header(format!( + "{} ({})", + row.server_name.0, row.worktree_root_name + )) + .entry( + SERVER_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + 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" + ); + } + menu = menu.entry( + SERVER_TRACE, None, cx.handler_for(&log_view, move |view, cx| { - view.show_logs_for_server(row.server_id, cx); + view.show_trace_for_server(row.server_id, cx); }), ); - if server_selected && row.logs_selected { - let selected_ix = menu.select_last(); - debug_assert_eq!( - Some(ix * 3 + 1), - selected_ix, - "Could not scroll to a just added LSP menu item" - ); - } - - menu = menu.custom_entry( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().child( - Checkbox::new( - ix, - if row.rpc_trace_enabled { - Selection::Selected - } else { - Selection::Unselected - }, - ) - .on_click(cx.listener_for( - &log_toolbar_view, - move |view, selection, cx| { - let enabled = matches!( - selection, + 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(); + move |cx| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().child( + Checkbox::new( + ix, + if row.rpc_trace_enabled { Selection::Selected - ); - view.toggle_rpc_logging_for_server( - row.server_id, - enabled, - cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, - cx.handler_for(&log_view, move |view, cx| { - view.show_rpc_trace_for_server(row.server_id, cx); - }), - ); - if server_selected && row.rpc_trace_selected { - let selected_ix = menu.select_last(); - debug_assert_eq!( - Some(ix * 3 + 2), - selected_ix, - "Could not scroll to a just added LSP menu item" + } else { + Selection::Unselected + }, + ) + .on_click(cx.listener_for( + &log_toolbar_view, + move |view, selection, cx| { + let enabled = matches!( + selection, + Selection::Selected + ); + view.toggle_rpc_logging_for_server( + row.server_id, + enabled, + cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_rpc_trace_for_server(row.server_id, cx); + }), ); + if server_selected && row.selected_entry == LogKind::Rpc { + let selected_ix = menu.select_last(); + debug_assert_eq!( + Some(ix * 4 + 3), + selected_ix, + "Could not scroll to a just added LSP menu item" + ); + } } - } - menu - }) - .into() + menu + }) + .into() + } }); - h_flex().size_full().child(lsp_menu).child( - div() - .child( - Button::new("clear_log_button", "Clear").on_click(cx.listener( - |this, _, cx| { - if let Some(log_view) = this.log_view.as_ref() { - log_view.update(cx, |log_view, cx| { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.clear(cx); - editor.set_read_only(true); - }); - }) - } - }, - )), - ) - .ml_2(), - ) + h_flex() + .size_full() + .child(lsp_menu) + .child( + div() + .child( + Button::new("clear_log_button", "Clear").on_click(cx.listener( + |this, _, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }, + )), + ) + .ml_2(), + ) + .child(log_view.update(cx, |this, _| { + if this.active_entry_kind == LogKind::Trace { + let log_view = log_view.clone(); + div().child( + PopoverMenu::new("lsp-trace-level-menu") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_trace_level_selector", + "Trace level", + )) + .menu({ + let log_view = log_view.clone(); + + move |cx| { + let id = log_view.read(cx).current_server_id?; + + let trace_level = log_view.update(cx, |this, cx| { + this.log_store.update(cx, |this, _| { + Some(this.get_language_server_state(id)?.trace_level) + }) + })?; + + ContextMenu::build(cx, |mut menu, _| { + let log_view = log_view.clone(); + + for (option, label) in [ + (TraceValue::Off, "Off"), + (TraceValue::Messages, "Messages"), + (TraceValue::Verbose, "Verbose"), + ] { + menu = menu.entry(label, None, { + let log_view = log_view.clone(); + move |cx| { + log_view.update(cx, |this, cx| { + if let Some(id) = this.current_server_id { + this.update_trace_level(id, option, cx); + } + }); + } + }); + if option == trace_level { + menu.select_last(); + } + } + + menu + }) + .into() + } + }), + ) + } else { + div() + } + })) } } const RPC_MESSAGES: &str = "RPC Messages"; const SERVER_LOGS: &str = "Server Logs"; +const SERVER_TRACE: &str = "Server Trace"; impl LspLogToolbarItemView { pub fn new() -> Self { @@ -979,7 +1140,7 @@ pub enum Event { NewServerLogEntry { id: LanguageServerId, entry: String, - is_rpc: bool, + kind: LogKind, }, } diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 395bbca530..58ff493c1f 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -8,6 +8,7 @@ use gpui::{Context, SemanticVersion, TestAppContext, VisualTestContext}; use language::{ tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName, }; +use lsp_log::LogKind; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; @@ -92,8 +93,8 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { .root_name() .to_string(), rpc_trace_enabled: false, - rpc_trace_selected: false, - logs_selected: true, + selected_entry: LogKind::Logs, + trace_level: lsp::TraceValue::Off, }] ); assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n"); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 99121868b2..bd02140beb 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -52,7 +52,7 @@ impl FluentBuilder for ContextMenu {} impl ContextMenu { pub fn build( cx: &mut WindowContext, - f: impl FnOnce(Self, &mut WindowContext) -> Self, + f: impl FnOnce(Self, &mut ViewContext) -> Self, ) -> View { cx.new_view(|cx| { let focus_handle = cx.focus_handle();