From a280a93cd8a80da187622c7b763aaf4d27fef519 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 20 Apr 2023 17:56:22 -0700 Subject: [PATCH] Start work on a language server log view --- Cargo.lock | 21 ++ Cargo.toml | 1 + crates/editor/src/editor.rs | 21 ++ crates/lsp_log/Cargo.toml | 29 +++ crates/lsp_log/src/lsp_log.rs | 374 ++++++++++++++++++++++++++++++++++ crates/project/src/project.rs | 36 ++-- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 7 +- 8 files changed, 477 insertions(+), 13 deletions(-) create mode 100644 crates/lsp_log/Cargo.toml create mode 100644 crates/lsp_log/src/lsp_log.rs diff --git a/Cargo.lock b/Cargo.lock index 7c2f043193..5713bddd0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3612,6 +3612,26 @@ dependencies = [ "url", ] +[[package]] +name = "lsp_log" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "editor", + "futures 0.3.25", + "gpui", + "language", + "lsp", + "project", + "serde", + "settings", + "theme", + "unindent", + "util", + "workspace", +] + [[package]] name = "mach" version = "0.3.2" @@ -8571,6 +8591,7 @@ dependencies = [ "libc", "log", "lsp", + "lsp_log", "node_runtime", "num_cpus", "outline", diff --git a/Cargo.toml b/Cargo.toml index 1c325fbb8e..94a0b0d402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", + "crates/lsp_log", "crates/media", "crates/menu", "crates/node_runtime", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 54ba2c1a9c..563c0aa132 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -511,6 +511,7 @@ pub struct Editor { workspace_id: Option, keymap_context_layers: BTreeMap, input_enabled: bool, + read_only: bool, leader_replica_id: Option, remote_id: Option, hover_state: HoverState, @@ -1283,6 +1284,7 @@ impl Editor { workspace_id: None, keymap_context_layers: Default::default(), input_enabled: true, + read_only: false, leader_replica_id: None, remote_id: None, hover_state: Default::default(), @@ -1425,6 +1427,10 @@ impl Editor { self.input_enabled = input_enabled; } + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } + fn selections_did_change( &mut self, local: bool, @@ -1533,6 +1539,10 @@ impl Editor { S: ToOffset, T: Into>, { + if self.read_only { + return; + } + self.buffer .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } @@ -1543,6 +1553,10 @@ impl Editor { S: ToOffset, T: Into>, { + if self.read_only { + return; + } + self.buffer.update(cx, |buffer, cx| { buffer.edit(edits, Some(AutoindentMode::EachLine), cx) }); @@ -1897,6 +1911,9 @@ impl Editor { pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { let text: Arc = text.into(); + if self.read_only { + return; + } if !self.input_enabled { cx.emit(Event::InputIgnored { text }); return; @@ -2282,6 +2299,10 @@ impl Editor { autoindent_mode: Option, cx: &mut ViewContext, ) { + if self.read_only { + return; + } + let text: Arc = text.into(); self.transact(cx, |this, cx| { let old_selections = this.selections.all_adjusted(cx); diff --git a/crates/lsp_log/Cargo.toml b/crates/lsp_log/Cargo.toml new file mode 100644 index 0000000000..4eeebef9a4 --- /dev/null +++ b/crates/lsp_log/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "lsp_log" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/lsp_log.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { path = "../editor" } +settings = { path = "../settings" } +theme = { path = "../theme" } +language = { path = "../language" } +project = { path = "../project" } +workspace = { path = "../workspace" } +gpui = { path = "../gpui" } +util = { path = "../util" } +lsp = { path = "../lsp" } +futures = { workspace = true } +serde = { workspace = true } +anyhow = "1.0" + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +unindent = "0.1.7" diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs new file mode 100644 index 0000000000..a50e9ad9a9 --- /dev/null +++ b/crates/lsp_log/src/lsp_log.rs @@ -0,0 +1,374 @@ +use collections::HashMap; +use editor::Editor; +use futures::{channel::mpsc, StreamExt}; +use gpui::{ + actions, + elements::{ + AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, + ParentElement, Stack, + }, + impl_internal_actions, + platform::MouseButton, + AppContext, Element, ElementBox, Entity, ModelHandle, RenderContext, View, ViewContext, + ViewHandle, +}; +use language::{Buffer, LanguageServerId, LanguageServerName}; +use project::{Project, WorktreeId}; +use settings::Settings; +use std::{borrow::Cow, sync::Arc}; +use theme::Theme; +use workspace::{ + item::{Item, ItemHandle}, + ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +const SEND_LINE: &str = "// Send:\n"; +const RECEIVE_LINE: &str = "// Receive:\n"; + +pub struct LspLogView { + enabled_logs: HashMap, + current_server_id: Option, + project: ModelHandle, + io_tx: mpsc::UnboundedSender<(LanguageServerId, bool, String)>, +} + +pub struct LspLogToolbarItemView { + log_view: Option>, + menu_open: bool, + project: ModelHandle, +} + +struct LogState { + buffer: ModelHandle, + editor: ViewHandle, + last_message_kind: Option, + _subscription: lsp::Subscription, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum MessageKind { + Send, + Receive, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct ActivateLog { + server_id: LanguageServerId, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct ToggleMenu; + +impl_internal_actions!(log, [ActivateLog, ToggleMenu]); +actions!(log, [OpenLanguageServerLogs]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(LspLogView::deploy); + cx.add_action(LspLogToolbarItemView::toggle_menu); + cx.add_action(LspLogToolbarItemView::activate_log_for_server); +} + +impl LspLogView { + pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { + let (io_tx, mut io_rx) = mpsc::unbounded(); + let this = Self { + enabled_logs: HashMap::default(), + current_server_id: None, + io_tx, + project, + }; + cx.spawn_weak(|this, mut cx| async move { + while let Some((language_server_id, is_output, mut message)) = io_rx.next().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + message.push('\n'); + this.on_io(language_server_id, is_output, &message, cx); + }) + } + } + }) + .detach(); + this + } + + fn deploy( + workspace: &mut Workspace, + _: &OpenLanguageServerLogs, + cx: &mut ViewContext, + ) { + let project = workspace.project().read(cx); + if project.is_remote() { + return; + } + + let log_view = cx.add_view(|cx| Self::new(workspace.project().clone(), cx)); + workspace.add_item(Box::new(log_view), cx); + } + + fn activate_log(&mut self, action: &ActivateLog, cx: &mut ViewContext) { + self.enable_logs_for_language_server(action.server_id, cx); + self.current_server_id = Some(action.server_id); + cx.notify(); + } + + fn on_io( + &mut self, + language_server_id: LanguageServerId, + is_received: bool, + message: &str, + cx: &mut ViewContext, + ) { + if let Some(state) = self.enabled_logs.get_mut(&language_server_id) { + state.buffer.update(cx, |buffer, cx| { + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + if state.last_message_kind != Some(kind) { + let len = buffer.len(); + let line = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + buffer.edit([(len..len, line)], None, cx); + state.last_message_kind = Some(kind); + } + let len = buffer.len(); + buffer.edit([(len..len, message)], None, cx); + }); + } + } + + pub fn enable_logs_for_language_server( + &mut self, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) { + if let Some(server) = self.project.read(cx).language_server_for_id(server_id) { + self.enabled_logs.entry(server_id).or_insert_with(|| { + let project = self.project.read(cx); + let io_tx = self.io_tx.clone(); + let language = project.languages().language_for_name("JSON"); + let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + cx.spawn({ + let buffer = buffer.clone(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }); + } + }) + .detach(); + let editor = cx.add_view(|cx| { + let mut editor = + Editor::for_buffer(buffer.clone(), Some(self.project.clone()), cx); + editor.set_read_only(true); + editor + }); + + LogState { + buffer, + editor, + last_message_kind: None, + _subscription: server.on_io(move |is_received, json| { + io_tx + .unbounded_send((server_id, is_received, json.to_string())) + .ok(); + }), + } + }); + } + } +} + +impl View for LspLogView { + fn ui_name() -> &'static str { + "LspLogView" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + if let Some(id) = self.current_server_id { + if let Some(log) = self.enabled_logs.get_mut(&id) { + return ChildView::new(&log.editor, cx).boxed(); + } + } + Empty::new().boxed() + } +} + +impl Item for LspLogView { + fn tab_content(&self, _: Option, style: &theme::Tab, _: &AppContext) -> ElementBox { + Label::new("Logs", style.label.clone()).boxed() + } +} + +impl ToolbarItemView for LspLogToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + self.menu_open = false; + if let Some(item) = active_pane_item { + if let Some(log_view) = item.downcast::() { + self.log_view = Some(log_view.clone()); + return ToolbarItemLocation::PrimaryLeft { + flex: Some((1., false)), + }; + } + } + self.log_view = None; + ToolbarItemLocation::Hidden + } +} + +impl View for LspLogToolbarItemView { + fn ui_name() -> &'static str { + "LspLogView" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let theme = cx.global::().theme.clone(); + let Some(log_view) = self.log_view.as_ref() else { return Empty::new().boxed() }; + let project = self.project.read(cx); + let mut language_servers = project.language_servers().collect::>(); + language_servers.sort_by_key(|a| a.0); + + let current_server_id = log_view.read(cx).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()) + } else { + None + } + }); + + Stack::new() + .with_child(Self::render_language_server_menu_header( + current_server, + &self.project, + &theme, + cx, + )) + .with_children(if self.menu_open { + Some( + Overlay::new( + Flex::column() + .with_children(language_servers.into_iter().filter_map( + |(id, name, worktree_id)| { + Self::render_language_server_menu_item( + id, + name, + worktree_id, + &self.project, + &theme, + cx, + ) + }, + )) + .contained() + .with_style(theme.contacts_popover.container) + .constrained() + .with_width(200.) + .with_height(400.) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopRight) + .with_z_index(999) + .aligned() + .bottom() + .right() + .boxed(), + ) + } else { + None + }) + .boxed() + } +} + +impl LspLogToolbarItemView { + pub fn new(project: ModelHandle) -> Self { + Self { + menu_open: false, + log_view: None, + project, + } + } + + fn toggle_menu(&mut self, _: &ToggleMenu, cx: &mut ViewContext) { + self.menu_open = !self.menu_open; + cx.notify(); + } + + fn activate_log_for_server(&mut self, action: &ActivateLog, cx: &mut ViewContext) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |log_view, cx| { + log_view.activate_log(action, cx); + }); + self.menu_open = false; + } + cx.notify(); + } + + fn render_language_server_menu_header( + current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId)>, + project: &ModelHandle, + theme: &Arc, + cx: &mut RenderContext, + ) -> ElementBox { + 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()) + }) + .unwrap_or_else(|| "No server selected".into()); + Label::new(label, theme.context_menu.item.default.label.clone()).boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleMenu); + }) + .boxed() + } + + fn render_language_server_menu_item( + id: LanguageServerId, + name: LanguageServerName, + worktree_id: WorktreeId, + project: &ModelHandle, + theme: &Arc, + cx: &mut RenderContext, + ) -> Option { + 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()); + + Some( + MouseEventHandler::::new(id.0, cx, move |state, cx| { + Label::new(label, theme.context_menu.item.default.label.clone()).boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ActivateLog { server_id: id }) + }) + .boxed(), + ) + } +} + +impl Entity for LspLogView { + type Event = (); +} + +impl Entity for LspLogToolbarItemView { + type Event = (); +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index afd0b3bbae..aad9a50856 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -185,6 +185,8 @@ pub struct Collaborator { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + LanguageServerAdded(LanguageServerId), + LanguageServerRemoved(LanguageServerId), ActiveEntryChanged(Option), WorktreeAdded, WorktreeRemoved(WorktreeId), @@ -1869,7 +1871,7 @@ impl Project { let next_snapshot = buffer.text_snapshot(); let language_servers: Vec<_> = self - .language_servers_iter_for_buffer(buffer, cx) + .language_servers_for_buffer(buffer, cx) .map(|i| i.1.clone()) .collect(); @@ -6279,7 +6281,25 @@ impl Project { } } - pub fn language_servers_iter_for_buffer( + pub fn language_servers( + &self, + ) -> impl '_ + Iterator { + self.language_server_ids + .iter() + .map(|((worktree_id, server_name), server_id)| { + (*server_id, server_name.clone(), *worktree_id) + }) + } + + pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { + if let LanguageServerState::Running { server, .. } = self.language_servers.get(&id)? { + Some(server.clone()) + } else { + None + } + } + + pub fn language_servers_for_buffer( &self, buffer: &Buffer, cx: &AppContext, @@ -6299,20 +6319,12 @@ impl Project { }) } - fn language_servers_for_buffer( - &self, - buffer: &Buffer, - cx: &AppContext, - ) -> Vec<(&Arc, &Arc)> { - self.language_servers_iter_for_buffer(buffer, cx).collect() - } - fn primary_language_servers_for_buffer( &self, buffer: &Buffer, cx: &AppContext, ) -> Option<(&Arc, &Arc)> { - self.language_servers_iter_for_buffer(buffer, cx).next() + self.language_servers_for_buffer(buffer, cx).next() } fn language_server_for_buffer( @@ -6321,7 +6333,7 @@ impl Project { server_id: LanguageServerId, cx: &AppContext, ) -> Option<(&Arc, &Arc)> { - self.language_servers_iter_for_buffer(buffer, cx) + self.language_servers_for_buffer(buffer, cx) .find(|(_, s)| s.server_id() == server_id) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index abd9dd10d9..fe6f7a8758 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -46,6 +46,7 @@ journal = { path = "../journal" } language = { path = "../language" } language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } +lsp_log = { path = "../lsp_log" } node_runtime = { path = "../node_runtime" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c6cfd8c109..c035ac07dd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -262,6 +262,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { ); activity_indicator::init(cx); copilot_button::init(cx); + lsp_log::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); settings::KeymapFileContent::load_defaults(cx); } @@ -273,7 +274,7 @@ pub fn initialize_workspace( ) { let workspace_handle = cx.handle(); cx.subscribe(&workspace_handle, { - move |_, _, event, cx| { + move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { @@ -287,6 +288,10 @@ 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()) + }); + toolbar.add_item(lsp_log_item, cx); }) }); }