diff --git a/Cargo.lock b/Cargo.lock index a7cbf1ec4f..b26a307375 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3764,8 +3764,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/src/lsp.rs b/crates/lsp/src/lsp.rs index 691203d5e8..39e65c6321 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/Cargo.toml b/crates/lsp_log/Cargo.toml index 6f47057b44..46f6006a23 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 db41c6ff4d..1071ed435a 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -1,4 +1,7 @@ -use collections::{hash_map, HashMap}; +#[cfg(test)] +mod lsp_log_tests; + +use collections::HashMap; use editor::Editor; use futures::{channel::mpsc, StreamExt}; use gpui::{ @@ -12,28 +15,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 +50,7 @@ struct LogStoreLanguageServer { pub struct LspLogView { log_store: ModelHandle, current_server_id: Option, + is_showing_rpc_trace: bool, editor: Option>, project: ModelHandle, } @@ -49,7 +58,6 @@ pub struct LspLogView { pub struct LspLogToolbarItemView { log_view: Option>, menu_open: bool, - project: ModelHandle, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -58,10 +66,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 +103,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 +134,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 +255,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 +293,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 +321,62 @@ 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.update(cx, |log_set, cx| { - log_set.enable_logs_for_language_server(&self.project, server_id, cx) - }); + 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); @@ -236,7 +387,28 @@ impl LspLogView { } } - fn toggle_logging_for_server( + 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_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); + editor.move_to_end(&Default::default(), cx); + editor + })); + cx.notify(); + } + } + + fn toggle_rpc_trace_for_server( &mut self, server_id: LanguageServerId, enabled: bool, @@ -244,11 +416,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 +481,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 +503,6 @@ impl View for LspLogToolbarItemView { Stack::new() .with_child(Self::render_language_server_menu_header( current_server, - &self.project, &theme, cx, )) @@ -346,22 +511,20 @@ 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) + .with_style(theme.lsp_log_menu.container) .constrained() .with_width(400.) .with_height(400.) @@ -388,12 +551,14 @@ impl View for LspLogToolbarItemView { } } +const RPC_MESSAGES: &str = "RPC Messages"; +const SERVER_LOGS: &str = "Server Logs"; + impl LspLogToolbarItemView { - pub fn new(project: ModelHandle) -> Self { + pub fn new() -> Self { Self { menu_open: false, log_view: None, - project, } } @@ -410,10 +575,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,39 +587,49 @@ 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(), + 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| { @@ -466,46 +640,75 @@ impl LspLogToolbarItemView { fn render_language_server_menu_item( id: LanguageServerId, name: LanguageServerName, - worktree_id: WorktreeId, - logging_enabled: bool, - is_selected: bool, - project: &ModelHandle, + worktree: ModelHandle, + rpc_trace_enabled: bool, + 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) + Flex::column() + .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) + .constrained() + .with_height(theme.lsp_log_menu.row_height) }) - .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, _| { + let style = theme.lsp_log_menu.item.style_for(state, logs_selected); + 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| { + view.show_logs_for_server(id, cx); + }), + ) + .with_child( + MouseEventHandler::::new(id.0, cx, move |state, cx| { + let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected); + Flex::row() + .with_child( + Label::new(RPC_MESSAGES, style.text.clone()) + .constrained() + .with_height(theme.lsp_log_menu.row_height), + ) + .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(style.container) + .constrained() + .with_height(theme.lsp_log_menu.row_height) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.show_rpc_trace_for_server(id, 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 0000000000..4be0db456c --- /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/project/src/project.rs b/crates/project/src/project.rs index 3406a3248c..0ac50e7106 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, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a67e38893b..3c23c30ab9 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/theme/src/theme.rs b/crates/theme/src/theme.rs index f7df63ca09..9bd17910d2 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,16 @@ 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, + pub row_height: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 862767c0ee..ef8cde78a6 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 ecdd1b7a18..d15bace554 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); }) }); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 7398432ec3..886553d418 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 "../theme/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 0000000000..94dd4831b2 --- /dev/null +++ b/styles/src/styleTree/lspLogMenu.ts @@ -0,0 +1,42 @@ +import { ColorScheme } from "../theme/colorScheme" +import { background, border, text } from "./components" + +export default function contactsPanel(colorScheme: ColorScheme) { + let layer = colorScheme.middle + + return { + rowHeight: 30, + 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"), + }, + }, + } +}