From 78f9642ac2d609928aa939c1b7b123278d6b1995 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 7 Jun 2023 14:46:08 -0700 Subject: [PATCH 1/4] Emit project event when a language server sends a logMessage notification --- crates/project/src/project.rs | 60 +++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2b2271bf1ef03605ca6d577626a79898dfdcb6ac..8571ff033d9e907fb59c152f9ed13c0faecea683 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -245,10 +245,11 @@ pub struct Collaborator { pub replica_id: ReplicaId, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), LanguageServerRemoved(LanguageServerId), + LanguageServerLog(LanguageServerId, String), ActiveEntryChanged(Option), WorktreeAdded, WorktreeRemoved(WorktreeId), @@ -2454,18 +2455,23 @@ impl Project { LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; let language_server = pending_server.task.await.log_err()?; - let language_server = language_server - .initialize(initialization_options) - .await - .log_err()?; - let this = this.upgrade(&cx)?; + + language_server + .on_notification::({ + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerLog(server_id, params.message)) + }); + } + } + }) + .detach(); language_server .on_notification::({ - let this = this.downgrade(); let adapter = adapter.clone(); move |mut params, cx| { - let this = this; let adapter = adapter.clone(); cx.spawn(|mut cx| async move { adapter.process_diagnostics(&mut params).await; @@ -2517,8 +2523,7 @@ impl Project { // avoid stalling any language server like `gopls` which waits for a response // to these requests when initializing. language_server - .on_request::({ - let this = this.downgrade(); + .on_request::( move |params, mut cx| async move { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, _| { @@ -2532,12 +2537,11 @@ impl Project { }); } Ok(()) - } - }) + }, + ) .detach(); language_server - .on_request::({ - let this = this.downgrade(); + .on_request::( move |params, mut cx| async move { let this = this .upgrade(&cx) @@ -2555,24 +2559,15 @@ impl Project { } } Ok(()) - } - }) + }, + ) .detach(); language_server .on_request::({ - let this = this.downgrade(); let adapter = adapter.clone(); - let language_server = language_server.clone(); move |params, cx| { - Self::on_lsp_workspace_edit( - this, - params, - server_id, - adapter.clone(), - language_server.clone(), - cx, - ) + Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx) } }) .detach(); @@ -2582,7 +2577,6 @@ impl Project { language_server .on_notification::({ - let this = this.downgrade(); move |params, mut cx| { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { @@ -2598,6 +2592,10 @@ impl Project { }) .detach(); + let language_server = language_server + .initialize(initialization_options) + .await + .log_err()?; language_server .notify::( lsp::DidChangeConfigurationParams { @@ -2606,6 +2604,7 @@ impl Project { ) .ok(); + let this = this.upgrade(&cx)?; this.update(&mut cx, |this, cx| { // If the language server for this key doesn't match the server id, don't store the // server. Which will cause it to be dropped, killing the process @@ -2640,6 +2639,8 @@ impl Project { }, ); + cx.emit(Event::LanguageServerAdded(server_id)); + if let Some(project_id) = this.remote_id() { this.client .send(proto::StartLanguageServer { @@ -2765,6 +2766,7 @@ impl Project { cx.notify(); let server_state = self.language_servers.remove(&server_id); + cx.emit(Event::LanguageServerRemoved(server_id)); cx.spawn_weak(|this, mut cx| async move { let mut root_path = None; @@ -3109,12 +3111,14 @@ impl Project { params: lsp::ApplyWorkspaceEditParams, server_id: LanguageServerId, adapter: Arc, - language_server: Arc, mut cx: AsyncAppContext, ) -> Result { let this = this .upgrade(&cx) .ok_or_else(|| anyhow!("project project closed"))?; + let language_server = this + .read_with(&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, From 66f215cd135751e6f9c6285105437d0d06306f72 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 7 Jun 2023 14:48:08 -0700 Subject: [PATCH 2/4] Restructure LSP log view to show log messages in addition to RPC trace --- crates/lsp/src/lsp.rs | 9 + crates/lsp_log/src/lsp_log.rs | 446 ++++++++++++++++++++-------- crates/project/src/project_tests.rs | 9 + crates/workspace/src/workspace.rs | 2 +- crates/zed/src/zed.rs | 5 +- 5 files changed, 335 insertions(+), 136 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 691203d5e8f51cb29249c4321814febb651ad0f9..39e65c6321256e44447b3792fc0e1ce170e6f391 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -748,6 +748,15 @@ impl fmt::Display for LanguageServerId { } } +impl fmt::Debug for LanguageServer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LanguageServer") + .field("id", &self.server_id.0) + .field("name", &self.name) + .finish_non_exhaustive() + } +} + impl Drop for Subscription { fn drop(&mut self) { match self { diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs index db41c6ff4d475bf870f28a2fb366ff9dcd199783..2e54f4726d987a4a7c3fd2f0015a8c054bc89462 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -1,4 +1,4 @@ -use collections::{hash_map, HashMap}; +use collections::HashMap; use editor::Editor; use futures::{channel::mpsc, StreamExt}; use gpui::{ @@ -12,28 +12,33 @@ use gpui::{ ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; -use project::{Project, WorktreeId}; +use project::{Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; use workspace::{ item::{Item, ItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, }; const SEND_LINE: &str = "// Send:\n"; const RECEIVE_LINE: &str = "// Receive:\n"; struct LogStore { - projects: HashMap, LogStoreProject>, + projects: HashMap, ProjectState>, io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, } -struct LogStoreProject { - servers: HashMap, - _subscription: gpui::Subscription, +struct ProjectState { + servers: HashMap, + _subscriptions: [gpui::Subscription; 2], } -struct LogStoreLanguageServer { +struct LanguageServerState { + log_buffer: ModelHandle, + rpc_state: Option, +} + +struct LanguageServerRpcState { buffer: ModelHandle, last_message_kind: Option, _subscription: lsp::Subscription, @@ -42,6 +47,7 @@ struct LogStoreLanguageServer { pub struct LspLogView { log_store: ModelHandle, current_server_id: Option, + is_showing_rpc_trace: bool, editor: Option>, project: ModelHandle, } @@ -49,7 +55,6 @@ pub struct LspLogView { pub struct LspLogToolbarItemView { log_view: Option>, menu_open: bool, - project: ModelHandle, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -58,10 +63,36 @@ enum MessageKind { Receive, } +#[derive(Clone, Debug, PartialEq)] +struct LogMenuItem { + server_id: LanguageServerId, + server_name: LanguageServerName, + worktree: ModelHandle, + rpc_trace_enabled: bool, + rpc_trace_selected: bool, + logs_selected: bool, +} + actions!(log, [OpenLanguageServerLogs]); pub fn init(cx: &mut AppContext) { - let log_set = cx.add_model(|cx| LogStore::new(cx)); + let log_store = cx.add_model(|cx| LogStore::new(cx)); + + cx.subscribe_global::({ + let log_store = log_store.clone(); + move |event, cx| { + let workspace = &event.0; + if let Some(workspace) = workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(&project, cx); + }); + } + } + } + }) + .detach(); cx.add_action( move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| { @@ -69,7 +100,7 @@ pub fn init(cx: &mut AppContext) { if project.is_local() { workspace.add_item( Box::new(cx.add_view(|cx| { - LspLogView::new(workspace.project().clone(), log_set.clone(), cx) + LspLogView::new(workspace.project().clone(), log_store.clone(), cx) })), cx, ); @@ -100,34 +131,113 @@ impl LogStore { this } - pub fn has_enabled_logs_for_language_server( + pub fn add_project(&mut self, project: &ModelHandle, cx: &mut ModelContext) { + use project::Event::*; + + let weak_project = project.downgrade(); + self.projects.insert( + weak_project, + ProjectState { + servers: HashMap::default(), + _subscriptions: [ + cx.observe_release(&project, move |this, _, _| { + this.projects.remove(&weak_project); + }), + cx.subscribe(project, |this, project, event, cx| match event { + LanguageServerAdded(id) => { + this.add_language_server(&project, *id, cx); + } + LanguageServerRemoved(id) => { + this.remove_language_server(&project, *id, cx); + } + LanguageServerLog(id, message) => { + this.add_language_server_log(&project, *id, message, cx); + } + _ => {} + }), + ], + }, + ); + } + + fn add_language_server( + &mut self, + project: &ModelHandle, + id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option> { + let project_state = self.projects.get_mut(&project.downgrade())?; + Some( + project_state + .servers + .entry(id) + .or_insert_with(|| { + cx.notify(); + LanguageServerState { + rpc_state: None, + log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(), + } + }) + .log_buffer + .clone(), + ) + } + + fn add_language_server_log( + &mut self, + project: &ModelHandle, + id: LanguageServerId, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + let buffer = self.add_language_server(&project, id, cx)?; + buffer.update(cx, |buffer, cx| { + let len = buffer.len(); + let has_newline = message.ends_with("\n"); + buffer.edit([(len..len, message)], None, cx); + if !has_newline { + let len = buffer.len(); + buffer.edit([(len..len, "\n")], None, cx); + } + }); + cx.notify(); + Some(()) + } + + fn remove_language_server( + &mut self, + project: &ModelHandle, + id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option<()> { + let project_state = self.projects.get_mut(&project.downgrade())?; + project_state.servers.remove(&id); + cx.notify(); + Some(()) + } + + pub fn log_buffer_for_server( &self, project: &ModelHandle, server_id: LanguageServerId, - ) -> bool { - self.projects - .get(&project.downgrade()) - .map_or(false, |store| store.servers.contains_key(&server_id)) + ) -> Option> { + let weak_project = project.downgrade(); + let project_state = self.projects.get(&weak_project)?; + let server_state = project_state.servers.get(&server_id)?; + Some(server_state.log_buffer.clone()) } - pub fn enable_logs_for_language_server( + pub fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, cx: &mut ModelContext, ) -> Option> { - let server = project.read(cx).language_server_for_id(server_id)?; let weak_project = project.downgrade(); - let project_logs = match self.projects.entry(weak_project) { - hash_map::Entry::Occupied(entry) => entry.into_mut(), - hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject { - servers: HashMap::default(), - _subscription: cx.observe_release(&project, move |this, _, _| { - this.projects.remove(&weak_project); - }), - }), - }; - let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| { + let project_state = self.projects.get_mut(&weak_project)?; + let server_state = project_state.servers.get_mut(&server_id)?; + let server = project.read(cx).language_server_for_id(server_id)?; + let rpc_state = server_state.rpc_state.get_or_insert_with(|| { let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); @@ -142,33 +252,30 @@ impl LogStore { }) .detach(); - let project = project.downgrade(); - LogStoreLanguageServer { + LanguageServerRpcState { buffer, last_message_kind: None, _subscription: server.on_io(move |is_received, json| { io_tx - .unbounded_send((project, server_id, is_received, json.to_string())) + .unbounded_send((weak_project, server_id, is_received, json.to_string())) .ok(); }), } }); - Some(server_log_state.buffer.clone()) + Some(rpc_state.buffer.clone()) } - pub fn disable_logs_for_language_server( + pub fn disable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, _: &mut ModelContext, - ) { + ) -> Option<()> { let project = project.downgrade(); - if let Some(store) = self.projects.get_mut(&project) { - store.servers.remove(&server_id); - if store.servers.is_empty() { - self.projects.remove(&project); - } - } + let project_state = self.projects.get_mut(&project)?; + let server_state = project_state.servers.get_mut(&server_id)?; + server_state.rpc_state.take(); + Some(()) } fn on_io( @@ -183,7 +290,9 @@ impl LogStore { .projects .get_mut(&project)? .servers - .get_mut(&language_server_id)?; + .get_mut(&language_server_id)? + .rpc_state + .as_mut()?; state.buffer.update(cx, |buffer, cx| { let kind = if is_received { MessageKind::Receive @@ -209,23 +318,83 @@ impl LogStore { impl LspLogView { fn new( project: ModelHandle, - log_set: ModelHandle, - _: &mut ViewContext, + log_store: ModelHandle, + cx: &mut ViewContext, ) -> Self { - Self { + let server_id = log_store + .read(cx) + .projects + .get(&project.downgrade()) + .and_then(|project| project.servers.keys().copied().next()); + let mut this = Self { project, - log_store: log_set, + log_store, editor: None, current_server_id: None, + is_showing_rpc_trace: false, + }; + if let Some(server_id) = server_id { + this.show_logs_for_server(server_id, cx); } + this + } + + fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { + let log_store = self.log_store.read(cx); + let state = log_store.projects.get(&self.project.downgrade())?; + let mut rows = self + .project + .read(cx) + .language_servers() + .filter_map(|(server_id, language_server_name, worktree_id)| { + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + let state = state.servers.get(&server_id)?; + Some(LogMenuItem { + server_id, + server_name: language_server_name, + worktree, + 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), + }) + }) + .collect::>(); + rows.sort_by_key(|row| row.server_id); + rows.dedup_by_key(|row| row.server_id); + Some(rows) } fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { + let buffer = self + .log_store + .read(cx) + .log_buffer_for_server(&self.project, server_id); + if let Some(buffer) = buffer { + self.current_server_id = Some(server_id); + self.is_showing_rpc_trace = false; + self.editor = Some(cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx); + editor.set_read_only(true); + editor.move_to_end(&Default::default(), cx); + editor + })); + cx.notify(); + } + } + + fn show_rpc_trace_for_server( + &mut self, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) { let buffer = self.log_store.update(cx, |log_set, cx| { - log_set.enable_logs_for_language_server(&self.project, server_id, cx) + log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx) }); if let Some(buffer) = buffer { self.current_server_id = Some(server_id); + self.is_showing_rpc_trace = true; self.editor = Some(cx.add_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx); editor.set_read_only(true); @@ -236,7 +405,7 @@ impl LspLogView { } } - fn toggle_logging_for_server( + fn toggle_rpc_trace_for_server( &mut self, server_id: LanguageServerId, enabled: bool, @@ -244,11 +413,15 @@ impl LspLogView { ) { self.log_store.update(cx, |log_store, cx| { if enabled { - log_store.enable_logs_for_language_server(&self.project, server_id, cx); + log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx); } else { - log_store.disable_logs_for_language_server(&self.project, server_id, cx); + log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx); } }); + if !enabled && Some(server_id) == self.current_server_id { + self.show_logs_for_server(server_id, cx); + cx.notify(); + } } } @@ -305,28 +478,18 @@ impl View for LspLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; - let project = self.project.read(cx); let log_view = log_view.read(cx); - let log_store = log_view.log_store.read(cx); - let mut language_servers = project - .language_servers() - .map(|(id, name, worktree)| { - ( - id, - name, - worktree, - log_store.has_enabled_logs_for_language_server(&self.project, id), - ) - }) - .collect::>(); - language_servers.sort_by_key(|a| (a.0, a.2)); - language_servers.dedup_by_key(|a| a.0); + let menu_rows = self + .log_view + .as_ref() + .and_then(|view| view.read(cx).menu_items(cx)) + .unwrap_or_default(); let current_server_id = log_view.current_server_id; let current_server = current_server_id.and_then(|current_server_id| { - if let Ok(ix) = language_servers.binary_search_by_key(¤t_server_id, |e| e.0) { - Some(language_servers[ix].clone()) + if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) { + Some(menu_rows[ix].clone()) } else { None } @@ -337,7 +500,6 @@ impl View for LspLogToolbarItemView { Stack::new() .with_child(Self::render_language_server_menu_header( current_server, - &self.project, &theme, cx, )) @@ -346,20 +508,18 @@ impl View for LspLogToolbarItemView { Overlay::new( MouseEventHandler::::new(0, cx, move |_, cx| { Flex::column() - .with_children(language_servers.into_iter().filter_map( - |(id, name, worktree_id, logging_enabled)| { - Self::render_language_server_menu_item( - id, - name, - worktree_id, - logging_enabled, - Some(id) == current_server_id, - &self.project, - &theme, - cx, - ) - }, - )) + .with_children(menu_rows.into_iter().map(|row| { + Self::render_language_server_menu_item( + row.server_id, + row.server_name, + row.worktree, + row.rpc_trace_enabled, + row.logs_selected, + row.rpc_trace_selected, + &theme, + cx, + ) + })) .contained() .with_style(theme.context_menu.container) .constrained() @@ -389,11 +549,10 @@ impl View for LspLogToolbarItemView { } impl LspLogToolbarItemView { - pub fn new(project: ModelHandle) -> Self { + pub fn new() -> Self { Self { menu_open: false, log_view: None, - project, } } @@ -410,10 +569,9 @@ impl LspLogToolbarItemView { ) { if let Some(log_view) = &self.log_view { log_view.update(cx, |log_view, cx| { - log_view.toggle_logging_for_server(id, enabled, cx); + log_view.toggle_rpc_trace_for_server(id, enabled, cx); if !enabled && Some(id) == log_view.current_server_id { - log_view.current_server_id = None; - log_view.editor = None; + log_view.show_logs_for_server(id, cx); cx.notify(); } }); @@ -423,28 +581,31 @@ impl LspLogToolbarItemView { fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { if let Some(log_view) = &self.log_view { - log_view.update(cx, |log_view, cx| { - log_view.show_logs_for_server(id, cx); - }); + log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx)); + self.menu_open = false; + cx.notify(); + } + } + + fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx)); self.menu_open = false; + cx.notify(); } - cx.notify(); } fn render_language_server_menu_header( - current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>, - project: &ModelHandle, + current_server: Option, theme: &Arc, cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} MouseEventHandler::::new(0, cx, move |state, cx| { - let project = project.read(cx); let label: Cow = current_server - .and_then(|(_, server_name, worktree_id, _)| { - let worktree = project.worktree_for_id(worktree_id, cx)?; - let worktree = &worktree.read(cx); - Some(format!("{} - ({})", server_name.0, worktree.root_name()).into()) + .and_then(|row| { + let worktree = row.worktree.read(cx); + Some(format!("{} - ({})", row.server_name.0, worktree.root_name()).into()) }) .unwrap_or_else(|| "No server selected".into()); Label::new( @@ -466,46 +627,67 @@ impl LspLogToolbarItemView { fn render_language_server_menu_item( id: LanguageServerId, name: LanguageServerName, - worktree_id: WorktreeId, + worktree: ModelHandle, logging_enabled: bool, - is_selected: bool, - project: &ModelHandle, + logs_selected: bool, + rpc_trace_selected: bool, theme: &Arc, cx: &mut ViewContext, - ) -> Option> { + ) -> impl Element { enum ActivateLog {} - let project = project.read(cx); - let worktree = project.worktree_for_id(worktree_id, cx)?; - let worktree = &worktree.read(cx); - if !worktree.is_visible() { - return None; - } - let label = format!("{} - ({})", name.0, worktree.root_name()); + enum ActivateRpcTrace {} - Some( - MouseEventHandler::::new(id.0, cx, move |state, cx| { - let item_style = theme.context_menu.item.style_for(state, is_selected); - Flex::row() - .with_child(ui::checkbox_with_label::( - Empty::new(), - &theme.welcome.checkbox, - logging_enabled, - id.0, - cx, - move |this, enabled, cx| { - this.toggle_logging_for_server(id, enabled, cx); - }, - )) - .with_child(Label::new(label, item_style.label.clone()).aligned().left()) - .align_children_center() - .contained() - .with_style(item_style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.show_logs_for_server(id, cx); - }), - ) + let header = format!("{} - ({})", name.0, worktree.read(cx).root_name()); + + let item_style = &theme.context_menu.item.default; + Flex::column() + .with_child( + Label::new(header, item_style.label.clone()) + .aligned() + .left(), + ) + .with_child( + MouseEventHandler::::new(id.0, cx, move |state, _| { + let item_style = &theme.context_menu.item.style_for(state, logs_selected); + Label::new("logs", item_style.label.clone()) + .aligned() + .left() + .contained() + .with_style(item_style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.show_logs_for_server(id, cx); + }), + ) + .with_child( + MouseEventHandler::::new(id.0, cx, move |state, cx| { + let item_style = &theme.context_menu.item.style_for(state, rpc_trace_selected); + Flex::row() + .with_child(ui::checkbox_with_label::( + Empty::new(), + &theme.welcome.checkbox, + logging_enabled, + id.0, + cx, + move |this, enabled, cx| { + this.toggle_logging_for_server(id, enabled, cx); + }, + )) + .with_child( + Label::new("rpc trace", item_style.label.clone()) + .aligned() + .left(), + ) + .align_children_center() + .contained() + .with_style(item_style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.show_rpc_trace_for_server(id, cx); + }), + ) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a67e38893b2457bcec814834c5f4d0142ecd6b12..3c23c30ab973ad76c003e467503ed5dae47f1d65 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -826,6 +826,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { let mut events = subscribe(&project, cx); let fake_server = fake_servers.next().await.unwrap(); + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerAdded(LanguageServerId(0)), + ); + fake_server .start_progress(format!("{}/0", progress_token)) .await; @@ -953,6 +958,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC // Simulate the newly started server sending more diagnostics. let fake_server = fake_servers.next().await.unwrap(); + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerAdded(LanguageServerId(1)) + ); fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 862767c0ee8744bb0dca6893da38525e22b098cf..ef8cde78a64aa1260d6be8174b24eb418762ed4d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3500,7 +3500,7 @@ impl std::fmt::Debug for OpenPaths { } } -pub struct WorkspaceCreated(WeakViewHandle); +pub struct WorkspaceCreated(pub WeakViewHandle); pub fn activate_workspace_for_project( cx: &mut AsyncAppContext, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ecdd1b7a180cee33fa938a39d901022855fe538a..d15bace5545d4087ae708adf7b0504838cfe6e54 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -311,9 +311,8 @@ pub fn initialize_workspace( toolbar.add_item(submit_feedback_button, cx); let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); toolbar.add_item(feedback_info_text, cx); - let lsp_log_item = cx.add_view(|_| { - lsp_log::LspLogToolbarItemView::new(workspace.project().clone()) - }); + let lsp_log_item = + cx.add_view(|_| lsp_log::LspLogToolbarItemView::new()); toolbar.add_item(lsp_log_item, cx); }) }); From 817644eb20dbf7e004962954e3a5385a75529484 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 7 Jun 2023 16:55:29 -0700 Subject: [PATCH 3/4] Style new lsp log menu, add a test for it --- Cargo.lock | 2 + crates/lsp_log/Cargo.toml | 2 + crates/lsp_log/src/lsp_log.rs | 94 +++++++++++++++++----------- crates/lsp_log/src/lsp_log_tests.rs | 97 +++++++++++++++++++++++++++++ crates/theme/src/theme.rs | 10 +++ styles/src/styleTree/app.ts | 2 + styles/src/styleTree/lspLogMenu.ts | 41 ++++++++++++ 7 files changed, 210 insertions(+), 38 deletions(-) create mode 100644 crates/lsp_log/src/lsp_log_tests.rs create mode 100644 styles/src/styleTree/lspLogMenu.ts diff --git a/Cargo.lock b/Cargo.lock index c6d09871039c07d6f6e44dfb6b2ab20b6455ec69..c6d6fbb944904a779a731c90eabd33a263464465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3763,8 +3763,10 @@ name = "lsp_log" version = "0.1.0" dependencies = [ "anyhow", + "client", "collections", "editor", + "env_logger 0.9.3", "futures 0.3.28", "gpui", "language", diff --git a/crates/lsp_log/Cargo.toml b/crates/lsp_log/Cargo.toml index 6f47057b442df907c3dcc42fd78d8f104301f24b..46f6006a23efde3569311da8f936c65e40ea9a93 100644 --- a/crates/lsp_log/Cargo.toml +++ b/crates/lsp_log/Cargo.toml @@ -24,7 +24,9 @@ serde.workspace = true anyhow.workspace = true [dev-dependencies] +client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } +env_logger.workspace = true unindent.workspace = true diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs index 2e54f4726d987a4a7c3fd2f0015a8c054bc89462..c7a95cab1d8f277c73fc5f1a889562691aeff482 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +mod lsp_log_tests; + use collections::HashMap; use editor::Editor; use futures::{channel::mpsc, StreamExt}; @@ -521,7 +524,7 @@ impl View for LspLogToolbarItemView { ) })) .contained() - .with_style(theme.context_menu.container) + .with_style(theme.lsp_log_menu.container) .constrained() .with_width(400.) .with_height(400.) @@ -548,6 +551,9 @@ impl View for LspLogToolbarItemView { } } +const RPC_MESSAGES: &str = "RPC Messages"; +const SERVER_LOGS: &str = "Server Logs"; + impl LspLogToolbarItemView { pub fn new() -> Self { Self { @@ -605,18 +611,25 @@ impl LspLogToolbarItemView { let label: Cow = current_server .and_then(|row| { let worktree = row.worktree.read(cx); - Some(format!("{} - ({})", row.server_name.0, worktree.root_name()).into()) + Some( + format!( + "{} ({}) - {}", + row.server_name.0, + worktree.root_name(), + if row.rpc_trace_selected { + RPC_MESSAGES + } else { + SERVER_LOGS + }, + ) + .into(), + ) }) .unwrap_or_else(|| "No server selected".into()); - Label::new( - label, - theme - .context_menu - .item - .style_for(state, false) - .label - .clone(), - ) + let style = theme.lsp_log_menu.header.style_for(state, false); + Label::new(label, style.text.clone()) + .contained() + .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { @@ -628,7 +641,7 @@ impl LspLogToolbarItemView { id: LanguageServerId, name: LanguageServerName, worktree: ModelHandle, - logging_enabled: bool, + rpc_trace_enabled: bool, logs_selected: bool, rpc_trace_selected: bool, theme: &Arc, @@ -637,23 +650,25 @@ impl LspLogToolbarItemView { enum ActivateLog {} enum ActivateRpcTrace {} - let header = format!("{} - ({})", name.0, worktree.read(cx).root_name()); - - let item_style = &theme.context_menu.item.default; Flex::column() - .with_child( - Label::new(header, item_style.label.clone()) - .aligned() - .left(), - ) + .with_child({ + let style = &theme.lsp_log_menu.server; + Label::new( + format!("{} ({})", name.0, worktree.read(cx).root_name()), + style.text.clone(), + ) + .contained() + .with_style(style.container) + .aligned() + .left() + }) .with_child( MouseEventHandler::::new(id.0, cx, move |state, _| { - let item_style = &theme.context_menu.item.style_for(state, logs_selected); - Label::new("logs", item_style.label.clone()) - .aligned() - .left() + let style = theme.lsp_log_menu.item.style_for(state, logs_selected); + Flex::row() + .with_child(Label::new(SERVER_LOGS, style.text.clone()).aligned().left()) .contained() - .with_style(item_style.container) + .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { @@ -662,26 +677,29 @@ impl LspLogToolbarItemView { ) .with_child( MouseEventHandler::::new(id.0, cx, move |state, cx| { - let item_style = &theme.context_menu.item.style_for(state, rpc_trace_selected); + let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected); Flex::row() - .with_child(ui::checkbox_with_label::( - Empty::new(), - &theme.welcome.checkbox, - logging_enabled, - id.0, - cx, - move |this, enabled, cx| { - this.toggle_logging_for_server(id, enabled, cx); - }, - )) .with_child( - Label::new("rpc trace", item_style.label.clone()) + Label::new(RPC_MESSAGES, style.text.clone()) .aligned() .left(), ) + .with_child( + ui::checkbox_with_label::( + Empty::new(), + &theme.welcome.checkbox, + rpc_trace_enabled, + id.0, + cx, + move |this, enabled, cx| { + this.toggle_logging_for_server(id, enabled, cx); + }, + ) + .flex_float(), + ) .align_children_center() .contained() - .with_style(item_style.container) + .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { diff --git a/crates/lsp_log/src/lsp_log_tests.rs b/crates/lsp_log/src/lsp_log_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..4be0db456cb663d2f708bd0132877f3a9a0ffc20 --- /dev/null +++ b/crates/lsp_log/src/lsp_log_tests.rs @@ -0,0 +1,97 @@ +use super::*; +use gpui::{serde_json::json, TestAppContext}; +use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig}; +use project::FakeFs; +use settings::SettingsStore; + +#[gpui::test] +async fn test_lsp_logs(cx: &mut TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + + init_test(cx); + + let mut rust_language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_rust_servers = rust_language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-rust-language-server", + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/the-root", + json!({ + "test.rs": "", + "package.json": "", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::new(rust_language)); + }); + + let log_store = cx.add_model(|cx| LogStore::new(cx)); + log_store.update(cx, |store, cx| store.add_project(&project, cx)); + + let _rust_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/test.rs", cx) + }) + .await + .unwrap(); + + let mut language_server = fake_rust_servers.next().await.unwrap(); + language_server + .receive_notification::() + .await; + + let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); + + language_server.notify::(lsp::LogMessageParams { + message: "hello from the server".into(), + typ: lsp::MessageType::INFO, + }); + cx.foreground().run_until_parked(); + + log_view.read_with(cx, |view, cx| { + assert_eq!( + view.menu_items(cx).unwrap(), + &[LogMenuItem { + server_id: language_server.server.server_id(), + server_name: LanguageServerName("the-rust-language-server".into()), + worktree: project.read(cx).worktrees(cx).next().unwrap(), + rpc_trace_enabled: false, + rpc_trace_selected: false, + logs_selected: true, + }] + ); + assert_eq!( + view.editor.as_ref().unwrap().read(cx).text(cx), + "hello from the server\n" + ); + }); +} + +fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + client::init_settings(cx); + Project::init_settings(cx); + editor::init_settings(cx); + }); +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f7df63ca099d9a50cf39c565d9cb658aafe098a1..164a8cc1b21554710e4b54a73172080dc9c27c88 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -44,6 +44,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub contacts_popover: ContactsPopover, pub contact_list: ContactList, + pub lsp_log_menu: LspLogMenu, pub copilot: Copilot, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, @@ -244,6 +245,15 @@ pub struct ContactFinder { pub disabled_contact_button: IconButton, } +#[derive(Deserialize, Default)] +pub struct LspLogMenu { + #[serde(flatten)] + pub container: ContainerStyle, + pub header: Interactive, + pub server: ContainedText, + pub item: Interactive, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index a9700a8d9994f0b8f63b74862b8db26c873a37da..4c85d2109f9fa450b3bfc38fa9b1dda442bed5b6 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -17,6 +17,7 @@ import projectSharedNotification from "./projectSharedNotification" import tooltip from "./tooltip" import terminal from "./terminal" import contactList from "./contactList" +import lspLogMenu from "./lspLogMenu" import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" @@ -45,6 +46,7 @@ export default function app(colorScheme: ColorScheme): Object { contactsPopover: contactsPopover(colorScheme), contactFinder: contactFinder(colorScheme), contactList: contactList(colorScheme), + lspLogMenu: lspLogMenu(colorScheme), search: search(colorScheme), sharedScreen: sharedScreen(colorScheme), updateNotification: updateNotification(colorScheme), diff --git a/styles/src/styleTree/lspLogMenu.ts b/styles/src/styleTree/lspLogMenu.ts new file mode 100644 index 0000000000000000000000000000000000000000..de4555e629e7372c8db6ef865b52336c229a0b46 --- /dev/null +++ b/styles/src/styleTree/lspLogMenu.ts @@ -0,0 +1,41 @@ +import { ColorScheme } from "../themes/common/colorScheme" +import { background, border, text } from "./components" + +export default function contactsPanel(colorScheme: ColorScheme) { + let layer = colorScheme.middle + + return { + background: background(layer), + border: border(layer), + shadow: colorScheme.popoverShadow, + header: { + ...text(layer, "sans", { size: "sm" }), + padding: { left: 8, right: 8, top: 2, bottom: 2 }, + cornerRadius: 6, + background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + hover: { + background: background(layer, "hovered"), + ...text(layer, "sans", "hovered", { size: "sm" }), + } + }, + server: { + ...text(layer, "sans", { size: "sm" }), + padding: { left: 8, right: 8, top: 8, bottom: 8 }, + }, + item: { + ...text(layer, "sans", { size: "sm" }), + padding: { left: 18, right: 18, top: 2, bottom: 2 }, + hover: { + background: background(layer, "hovered"), + ...text(layer, "sans", "hovered", { size: "sm" }), + }, + active: { + background: background(layer, "active"), + }, + activeHover: { + background: background(layer, "active"), + }, + }, + } +} From 908de23b72cd8c5bd0e13fcb1079667f2155b0a8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 8 Jun 2023 08:58:57 -0700 Subject: [PATCH 4/4] Tweak LSP log menu styling --- crates/lsp_log/src/lsp_log.rs | 15 +++++++++------ crates/theme/src/theme.rs | 1 + styles/src/styleTree/lspLogMenu.ts | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs index c7a95cab1d8f277c73fc5f1a889562691aeff482..1071ed435a44258368bd0fd373905a55918b21b3 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -659,16 +659,17 @@ impl LspLogToolbarItemView { ) .contained() .with_style(style.container) - .aligned() - .left() + .constrained() + .with_height(theme.lsp_log_menu.row_height) }) .with_child( MouseEventHandler::::new(id.0, cx, move |state, _| { let style = theme.lsp_log_menu.item.style_for(state, logs_selected); - Flex::row() - .with_child(Label::new(SERVER_LOGS, style.text.clone()).aligned().left()) + Label::new(SERVER_LOGS, style.text.clone()) .contained() .with_style(style.container) + .constrained() + .with_height(theme.lsp_log_menu.row_height) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { @@ -681,8 +682,8 @@ impl LspLogToolbarItemView { Flex::row() .with_child( Label::new(RPC_MESSAGES, style.text.clone()) - .aligned() - .left(), + .constrained() + .with_height(theme.lsp_log_menu.row_height), ) .with_child( ui::checkbox_with_label::( @@ -700,6 +701,8 @@ impl LspLogToolbarItemView { .align_children_center() .contained() .with_style(style.container) + .constrained() + .with_height(theme.lsp_log_menu.row_height) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 164a8cc1b21554710e4b54a73172080dc9c27c88..9bd17910d2a166bbe56b2a6cfffcd404c686e1e9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -252,6 +252,7 @@ pub struct LspLogMenu { pub header: Interactive, pub server: ContainedText, pub item: Interactive, + pub row_height: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/lspLogMenu.ts b/styles/src/styleTree/lspLogMenu.ts index a6b8f7b1f9ef61324d63fd290a3152e2c1aa0375..94dd4831b259facbe01ff2bf5136202b8c30faad 100644 --- a/styles/src/styleTree/lspLogMenu.ts +++ b/styles/src/styleTree/lspLogMenu.ts @@ -5,6 +5,7 @@ export default function contactsPanel(colorScheme: ColorScheme) { let layer = colorScheme.middle return { + rowHeight: 30, background: background(layer), border: border(layer), shadow: colorScheme.popoverShadow,