From 43ea3b47a468ff0b39d9c7c1b4f28b128ffdb494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20Butkovi=C4=87?= Date: Mon, 21 Oct 2024 19:44:35 +0200 Subject: [PATCH] Implement logging for debug adapter clients (#45) * Implement RPC logging for debug adapter clients * Implement server logs for debugger servers * This cleans up the way we pass through the input and output readers for logging. So not each debug adapters have to map the AdapterLogIO fields. I also removed some specific when has logs from the client, because the client is not responsible for that. Removed an not needed/duplicated dependency Fix formatting & clippy This cleans up the way we pass through the input and output readers for logging. So not each debug adapters have to map the AdapterLogIO fields. I also removed some specific when has logs from the client, because the client is not responsible for that. Removed an not needed/duplicated dependency Fix formatting & clippy * Implement `has_adapter_logs` for each transport impl * Make adapter stdout logging work * Add conditional render for adapter log back * Oops forgot to pipe the output * Always enable rpc messages Previously, RPC messages were only stored when explicitly enabled, which occurred after the client was already running. This approach prevented debugging of requests sent during the initial connection period. By always enabling RPC messages, we ensure that all requests, including those during the connection phase, are captured and available for debugging purposes. This could help use debug when someone has troble getting a debug starting. This improvement could be particularly helpful in debugging scenarios where users encounter issues during the initial connection or startup phase of their debugging sessions. --------- Co-authored-by: Remco Smits Co-authored-by: Anthony Eid --- Cargo.lock | 17 + Cargo.toml | 2 + crates/dap/Cargo.toml | 2 + crates/dap/src/adapters.rs | 1 - crates/dap/src/client.rs | 14 +- crates/dap/src/transport.rs | 132 +++- crates/debugger_tools/Cargo.toml | 23 + crates/debugger_tools/src/dap_log.rs | 766 ++++++++++++++++++++ crates/debugger_tools/src/debugger_tools.rs | 8 + crates/project/src/project.rs | 18 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 + 13 files changed, 957 insertions(+), 30 deletions(-) create mode 100644 crates/debugger_tools/Cargo.toml create mode 100644 crates/debugger_tools/src/dap_log.rs create mode 100644 crates/debugger_tools/src/debugger_tools.rs diff --git a/Cargo.lock b/Cargo.lock index 9f89190e0ec7d3..e83e965252d2c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3354,10 +3354,12 @@ dependencies = [ "http_client", "log", "node_runtime", + "parking_lot", "schemars", "serde", "serde_json", "settings", + "smallvec", "smol", "task", ] @@ -3462,6 +3464,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "debugger_tools" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap", + "editor", + "futures 0.3.30", + "gpui", + "project", + "serde_json", + "workspace", +] + [[package]] name = "debugger_ui" version = "0.1.0" @@ -14690,6 +14706,7 @@ dependencies = [ "command_palette_hooks", "copilot", "db", + "debugger_tools", "debugger_ui", "dev_server_projects", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 505cba7c3611d8..a2ca527352df1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,6 +172,7 @@ members = [ # "tooling/xtask", + "crates/debugger_tools", ] default-members = ["crates/zed"] @@ -207,6 +208,7 @@ dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } db = { path = "crates/db" } debugger_ui = { path = "crates/debugger_ui" } +debugger_tools = { path = "crates/debugger_tools" } dev_server_projects = { path = "crates/dev_server_projects" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c3e6571c4d63a2..bf5c574dc66c3b 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -18,9 +18,11 @@ gpui.workspace = true http_client.workspace = true log.workspace = true node_runtime.workspace = true +parking_lot.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +smallvec.workspace = true smol.workspace = true task.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index ca4662bac61669..119dd113d781ff 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -6,7 +6,6 @@ use http_client::HttpClient; use node_runtime::NodeRuntime; use serde_json::Value; use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc}; - use task::DebugAdapterConfig; pub trait DapDelegate { diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 71343f1a44f7fb..672fa17c817c6c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,8 +1,9 @@ use crate::{ adapters::{DebugAdapter, DebugAdapterBinary}, - transport::TransportDelegate, + transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; + use dap_types::{ messages::{Message, Response}, requests::Request, @@ -163,4 +164,15 @@ impl DebugAdapterClient { pub async fn shutdown(&self) -> Result<()> { self.transport_delegate.shutdown().await } + + pub fn has_adapter_logs(&self) -> bool { + self.transport_delegate.has_adapter_logs() + } + + pub fn add_log_handler(&self, f: F, kind: LogKind) + where + F: 'static + Send + FnMut(IoKind, &str), + { + self.transport_delegate.add_log_handler(f, kind); + } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 7849870471d53e..1e767e2882cdca 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -6,12 +6,13 @@ use dap_types::{ }; use futures::{select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; use gpui::AsyncAppContext; +use smallvec::SmallVec; use smol::{ channel::{unbounded, Receiver, Sender}, io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, net::{TcpListener, TcpStream}, - process::{self, Child}, + process::{self, Child, ChildStderr, ChildStdout}, }; use std::{ borrow::BorrowMut, @@ -25,10 +26,23 @@ use task::TCPHost; use crate::adapters::DebugAdapterBinary; +pub type IoHandler = Box; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum LogKind { + Adapter, + Rpc, +} + +pub enum IoKind { + StdIn, + StdOut, + StdErr, +} + pub struct TransportParams { input: Box, output: Box, - error: Box, process: Child, } @@ -36,21 +50,21 @@ impl TransportParams { pub fn new( input: Box, output: Box, - error: Box, process: Child, ) -> Self { TransportParams { input, output, - error, process, } } } type Requests = Arc>>>>; +type LogHandlers = Arc>>; pub(crate) struct TransportDelegate { + log_handlers: LogHandlers, current_requests: Requests, pending_requests: Requests, transport: Box, @@ -64,6 +78,7 @@ impl TransportDelegate { transport, server_tx: None, process: Default::default(), + log_handlers: Default::default(), current_requests: Default::default(), pending_requests: Default::default(), } @@ -74,25 +89,31 @@ impl TransportDelegate { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result<(Receiver, Sender)> { - let params = self.transport.start(binary, cx).await?; + let mut params = self.transport.start(binary, cx).await?; let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - self.process = Arc::new(Mutex::new(Some(params.process))); - self.server_tx = Some(server_tx.clone()); + if let Some(stdout) = params.process.stdout.take() { + cx.background_executor() + .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) + .detach(); + } cx.background_executor() .spawn(Self::handle_output( params.output, client_tx, self.pending_requests.clone(), + self.log_handlers.clone(), )) .detach(); - cx.background_executor() - .spawn(Self::handle_error(params.error)) - .detach(); + if let Some(stderr) = params.process.stderr.take() { + cx.background_executor() + .spawn(Self::handle_error(stderr, self.log_handlers.clone())) + .detach(); + } cx.background_executor() .spawn(Self::handle_input( @@ -100,9 +121,13 @@ impl TransportDelegate { client_rx, self.current_requests.clone(), self.pending_requests.clone(), + self.log_handlers.clone(), )) .detach(); + self.process = Arc::new(Mutex::new(Some(params.process))); + self.server_tx = Some(server_tx.clone()); + Ok((server_rx, server_tx)) } @@ -126,11 +151,27 @@ impl TransportDelegate { } } + async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) -> Result<()> { + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + while reader.read_line(&mut line).await? > 0 { + for (kind, handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Adapter) { + handler(IoKind::StdOut, line.as_str()); + } + } + line.truncate(0); + } + + Ok(()) + } + async fn handle_input( mut server_stdin: Box, client_rx: Receiver, current_requests: Requests, pending_requests: Requests, + log_handlers: LogHandlers, ) -> Result<()> { while let Ok(mut payload) = client_rx.recv().await { if let Message::Request(request) = payload.borrow_mut() { @@ -141,6 +182,12 @@ impl TransportDelegate { let message = serde_json::to_string(&payload)?; + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdIn, &message); + } + } + server_stdin .write_all( format!("Content-Length: {}\r\n\r\n{}", message.len(), message).as_bytes(), @@ -157,11 +204,12 @@ impl TransportDelegate { mut server_stdout: Box, client_tx: Sender, pending_requests: Requests, + log_handlers: LogHandlers, ) -> Result<()> { let mut recv_buffer = String::new(); while let Ok(message) = - Self::receive_server_message(&mut server_stdout, &mut recv_buffer).await + Self::receive_server_message(&mut server_stdout, &mut recv_buffer, &log_handlers).await { match message { Message::Response(res) => { @@ -183,13 +231,22 @@ impl TransportDelegate { Ok(()) } - async fn handle_error(mut stderr: Box) -> Result<()> { + async fn handle_error(stderr: ChildStderr, log_handlers: LogHandlers) -> Result<()> { let mut buffer = String::new(); + + let mut reader = BufReader::new(stderr); + loop { buffer.truncate(0); - if stderr.read_line(&mut buffer).await? == 0 { + if reader.read_line(&mut buffer).await? == 0 { return Err(anyhow!("debugger error stream closed")); } + + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdErr, buffer.as_str()); + } + } } } @@ -212,6 +269,7 @@ impl TransportDelegate { async fn receive_server_message( reader: &mut Box, buffer: &mut String, + log_handlers: &LogHandlers, ) -> Result { let mut content_length = None; loop { @@ -248,8 +306,15 @@ impl TransportDelegate { .await .with_context(|| "reading after a loop")?; - let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; - Ok(serde_json::from_str::(msg)?) + let message = std::str::from_utf8(&content).context("invalid utf8 from server")?; + + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdOut, &message); + } + } + + Ok(serde_json::from_str::(message)?) } pub async fn shutdown(&self) -> Result<()> { @@ -274,6 +339,18 @@ impl TransportDelegate { anyhow::Ok(()) } + + pub fn has_adapter_logs(&self) -> bool { + self.transport.has_adapter_logs() + } + + pub fn add_log_handler(&self, f: F, kind: LogKind) + where + F: 'static + Send + FnMut(IoKind, &str), + { + let mut log_handlers = self.log_handlers.lock(); + log_handlers.push((kind, Box::new(f))); + } } #[async_trait(?Send)] @@ -283,6 +360,8 @@ pub trait Transport: 'static + Send + Sync { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result; + + fn has_adapter_logs(&self) -> bool; } pub struct TcpTransport { @@ -336,19 +415,14 @@ impl Transport for TcpTransport { command .stdin(Stdio::null()) - .stdout(Stdio::null()) + .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); - let mut process = command + let process = command .spawn() .with_context(|| "failed to start debug adapter.")?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; - let address = SocketAddrV4::new( host_address, port.ok_or(anyhow!("Port is required to connect to TCP server"))?, @@ -376,10 +450,13 @@ impl Transport for TcpTransport { Ok(TransportParams::new( Box::new(tx), Box::new(BufReader::new(rx)), - Box::new(BufReader::new(stderr)), process, )) } + + fn has_adapter_logs(&self) -> bool { + true + } } pub struct StdioTransport {} @@ -425,18 +502,17 @@ impl Transport for StdioTransport { .stdout .take() .ok_or_else(|| anyhow!("Failed to open stdout"))?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; log::info!("Debug adapter has connected to stdio adapter"); Ok(TransportParams::new( Box::new(stdin), Box::new(BufReader::new(stdout)), - Box::new(BufReader::new(stderr)), process, )) } + + fn has_adapter_logs(&self) -> bool { + false + } } diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml new file mode 100644 index 00000000000000..bdcfc6bc3d23e5 --- /dev/null +++ b/crates/debugger_tools/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "debugger_tools" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/debugger_tools.rs" +doctest = false + +[dependencies] +gpui.workspace = true +workspace.workspace = true +editor.workspace = true +project.workspace = true +dap.workspace = true +serde_json.workspace = true +futures.workspace = true +anyhow.workspace = true diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs new file mode 100644 index 00000000000000..6a61edf1ad2620 --- /dev/null +++ b/crates/debugger_tools/src/dap_log.rs @@ -0,0 +1,766 @@ +use dap::{ + client::{DebugAdapterClient, DebugAdapterClientId}, + transport::{IoKind, LogKind}, +}; +use editor::{Editor, EditorEvent}; +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; +use gpui::{ + actions, div, AnchorCorner, AppContext, Context, EventEmitter, FocusHandle, FocusableView, + IntoElement, Model, ModelContext, ParentElement, Render, SharedString, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, +}; +use project::{search::SearchQuery, Project}; +use std::{ + borrow::Cow, + collections::{HashMap, VecDeque}, + sync::Arc, +}; +use workspace::{ + item::Item, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + ui::{h_flex, Button, Clickable, ContextMenu, Label, PopoverMenu}, + ToolbarItemEvent, ToolbarItemView, Workspace, +}; + +struct DapLogView { + editor: View, + focus_handle: FocusHandle, + log_store: Model, + editor_subscriptions: Vec, + current_view: Option<(DebugAdapterClientId, LogKind)>, + project: Model, + _subscriptions: Vec, +} + +struct LogStore { + projects: HashMap, ProjectState>, + debug_clients: HashMap, + rpc_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + adapter_log_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, +} + +struct ProjectState { + _subscriptions: [gpui::Subscription; 2], +} + +struct DebugAdapterState { + log_messages: VecDeque, + rpc_messages: RpcMessages, +} + +struct RpcMessages { + messages: VecDeque, + last_message_kind: Option, +} + +impl RpcMessages { + const MESSAGE_QUEUE_LIMIT: usize = 255; + + fn new() -> Self { + Self { + last_message_kind: None, + messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), + } + } +} + +const SEND: &str = "// Send"; +const RECEIVE: &str = "// Receive"; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum MessageKind { + Send, + Receive, +} + +impl MessageKind { + fn label(&self) -> &'static str { + match self { + Self::Send => SEND, + Self::Receive => RECEIVE, + } + } +} + +impl DebugAdapterState { + fn new() -> Self { + Self { + log_messages: VecDeque::new(), + rpc_messages: RpcMessages::new(), + } + } +} + +impl LogStore { + fn new(cx: &ModelContext) -> Self { + let (rpc_tx, mut rpc_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = rpc_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_rpc_log(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + let (adapter_log_tx, mut adapter_log_rx) = + unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = adapter_log_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_adapter_log(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + Self { + rpc_tx, + adapter_log_tx, + projects: HashMap::new(), + debug_clients: HashMap::new(), + } + } + + fn on_rpc_log( + &mut self, + client_id: DebugAdapterClientId, + io_kind: IoKind, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); + + Some(()) + } + + fn on_adapter_log( + &mut self, + client_id: DebugAdapterClientId, + io_kind: IoKind, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); + + Some(()) + } + + pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { + let weak_project = project.downgrade(); + self.projects.insert( + project.downgrade(), + ProjectState { + _subscriptions: [ + cx.observe_release(project, move |this, _, _| { + this.projects.remove(&weak_project); + }), + cx.subscribe(project, |this, project, event, cx| match event { + project::Event::DebugClientStarted(client_id) => { + this.add_debug_client( + *client_id, + project.read(cx).debug_client_for_id(client_id, cx), + ); + } + project::Event::DebugClientStopped(id) => { + this.remove_debug_client(*id, cx); + } + + _ => {} + }), + ], + }, + ); + } + + fn get_debug_adapter_state( + &mut self, + id: DebugAdapterClientId, + ) -> Option<&mut DebugAdapterState> { + self.debug_clients.get_mut(&id) + } + + fn add_debug_client_message( + &mut self, + id: DebugAdapterClientId, + io_kind: IoKind, + message: String, + cx: &mut ModelContext, + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; + + let kind = match io_kind { + IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, + IoKind::StdIn => MessageKind::Send, + }; + + let rpc_messages = &mut debug_client_state.rpc_messages; + if rpc_messages.last_message_kind != Some(kind) { + Self::add_debug_client_entry( + &mut rpc_messages.messages, + id, + kind.label().to_string(), + LogKind::Rpc, + cx, + ); + rpc_messages.last_message_kind = Some(kind); + } + Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); + } + + fn add_debug_client_log( + &mut self, + id: DebugAdapterClientId, + io_kind: IoKind, + message: String, + cx: &mut ModelContext, + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; + + let mut log_messages = &mut debug_client_state.log_messages; + + let message = match io_kind { + IoKind::StdErr => { + let mut message = message.clone(); + message.insert_str(0, "stderr: "); + message + } + _ => message, + }; + + Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Adapter, cx); + } + + fn add_debug_client_entry( + log_lines: &mut VecDeque, + id: DebugAdapterClientId, + message: String, + kind: LogKind, + cx: &mut ModelContext, + ) { + while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { + log_lines.pop_front(); + } + let entry: &str = message.as_ref(); + let entry = entry.to_string(); + log_lines.push_back(message); + + cx.emit(Event::NewLogEntry { id, entry, kind }); + cx.notify(); + } + + fn add_debug_client( + &mut self, + client_id: DebugAdapterClientId, + client: Option>, + ) -> Option<&mut DebugAdapterState> { + let client_state = self + .debug_clients + .entry(client_id) + .or_insert_with(DebugAdapterState::new); + + if let Some(client) = client { + let io_tx = self.rpc_tx.clone(); + + client.add_log_handler( + move |io_kind, message| { + io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Rpc, + ); + + let log_io_tx = self.adapter_log_tx.clone(); + client.add_log_handler( + move |io_kind, message| { + log_io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Adapter, + ); + } + + Some(client_state) + } + + fn remove_debug_client(&mut self, id: DebugAdapterClientId, cx: &mut ModelContext) { + self.debug_clients.remove(&id); + cx.notify(); + } + + fn log_messages_for_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) + } + + fn rpc_messages_for_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + Some( + &mut self + .debug_clients + .get_mut(&client_id)? + .rpc_messages + .messages, + ) + } +} + +pub struct DapLogToolbarItemView { + log_view: Option>, +} + +impl DapLogToolbarItemView { + pub fn new() -> Self { + Self { log_view: None } + } +} + +impl Render for DapLogToolbarItemView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(log_view) = self.log_view.clone() else { + return div(); + }; + + let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| { + ( + log_view.menu_items(cx).unwrap_or_default(), + log_view.current_view.map(|(client_id, _)| client_id), + ) + }); + + let current_client = current_client_id.and_then(|current_client_id| { + if let Ok(ix) = menu_rows.binary_search_by_key(¤t_client_id, |e| e.client_id) { + Some(&menu_rows[ix]) + } else { + None + } + }); + + let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "debug_server_menu_header", + current_client + .map(|row| { + Cow::Owned(format!( + "{} - {}", + row.client_name, + match row.selected_entry { + LogKind::Adapter => ADAPTER_LOGS, + LogKind::Rpc => RPC_MESSAGES, + } + )) + }) + .unwrap_or_else(|| "No server selected".into()), + )) + .menu(move |cx| { + let log_view = log_view.clone(); + let menu_rows = menu_rows.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for row in menu_rows.into_iter() { + menu = menu.header(row.client_name.to_string()); + + if row.has_adapter_logs { + menu = menu.entry( + ADAPTER_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_log_messages_for_server(row.client_id, cx); + }), + ); + } + + menu = menu.custom_entry( + move |_| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .into_any_element() + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_rpc_trace_for_server(row.client_id, cx); + }), + ); + } + menu + }) + .into() + }); + + h_flex().size_full().child(dap_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(), + ) + } +} + +impl EventEmitter for DapLogToolbarItemView {} + +impl ToolbarItemView for DapLogToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::item::ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + if let Some(item) = active_pane_item { + if let Some(log_view) = item.downcast::() { + self.log_view = Some(log_view.clone()); + return workspace::ToolbarItemLocation::PrimaryLeft; + } + } + self.log_view = None; + + cx.notify(); + + workspace::ToolbarItemLocation::Hidden + } +} + +impl DapLogView { + pub fn new( + project: Model, + log_store: Model, + cx: &mut ViewContext, + ) -> Self { + let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx); + + let focus_handle = cx.focus_handle(); + + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + Event::NewLogEntry { id, entry, kind } => { + if log_view.current_view == Some((*id, *kind)) { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + let last_point = editor.buffer().read(cx).len(cx); + editor.edit( + vec![ + (last_point..last_point, entry.trim()), + (last_point..last_point, "\n"), + ], + cx, + ); + editor.set_read_only(true); + }); + } + } + }); + + Self { + editor, + focus_handle, + project, + log_store, + editor_subscriptions, + current_view: None, + _subscriptions: vec![events_subscriptions], + } + } + + fn editor_for_logs( + log_contents: String, + cx: &mut ViewContext, + ) -> (View, Vec) { + let editor = cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.set_text(log_contents, cx); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.set_read_only(true); + editor.set_show_inline_completions(Some(false), cx); + editor + }); + let editor_subscription = cx.subscribe( + &editor, + |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, DapLogView>| { + cx.emit(event.clone()) + }, + ); + let search_subscription = cx.subscribe( + &editor, + |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, DapLogView>| { + cx.emit(event.clone()) + }, + ); + (editor, vec![editor_subscription, search_subscription]) + } + + fn menu_items(&self, cx: &AppContext) -> Option> { + let mut rows = self + .project + .read(cx) + .debug_clients(cx) + .map(|client| DapMenuItem { + client_id: client.id(), + client_name: client.config().kind.to_string(), + selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), + has_adapter_logs: client.has_adapter_logs(), + }) + .collect::>(); + rows.sort_by_key(|row| row.client_id); + rows.dedup_by_key(|row| row.client_id); + Some(rows) + } + + fn show_rpc_trace_for_server( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let rpc_log = self.log_store.update(cx, |log_store, _| { + log_store + .rpc_messages_for_client(client_id) + .map(|state| log_contents(&state)) + }); + if let Some(rpc_log) = rpc_log { + self.current_view = Some((client_id, 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 + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton") + .update(cx, |_, cx| { + cx.spawn({ + let buffer = cx.handle(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }) + } + }) + .detach_and_log_err(cx); + }); + + self.editor = editor; + self.editor_subscriptions = editor_subscriptions; + cx.notify(); + } + + cx.focus(&self.focus_handle); + } + + fn show_log_messages_for_server( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let message_log = self.log_store.update(cx, |log_store, _| { + log_store + .log_messages_for_client(client_id) + .map(|state| log_contents(&state)) + }); + if let Some(message_log) = message_log { + self.current_view = Some((client_id, LogKind::Adapter)); + let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, cx); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton"); + + self.editor = editor; + self.editor_subscriptions = editor_subscriptions; + cx.notify(); + } + + cx.focus(&self.focus_handle); + } +} + +fn log_contents(lines: &VecDeque) -> String { + let (a, b) = lines.as_slices(); + let a = a.iter().map(move |v| v.as_ref()); + let b = b.iter().map(move |v| v.as_ref()); + a.chain(b).fold(String::new(), |mut acc, el| { + acc.push_str(el); + acc.push('\n'); + acc + }) +} + +#[derive(Clone, PartialEq)] +pub(crate) struct DapMenuItem { + pub client_id: DebugAdapterClientId, + pub client_name: String, + pub has_adapter_logs: bool, + pub selected_entry: LogKind, +} + +const ADAPTER_LOGS: &str = "Adapter Logs"; +const RPC_MESSAGES: &str = "RPC Messages"; + +impl Render for DapLogView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + self.editor + .update(cx, |editor, cx| editor.render(cx).into_any_element()) + } +} + +actions!(debug, [OpenDebuggerServerLogs]); + +pub fn init(cx: &mut AppContext) { + let log_store = cx.new_model(|cx| LogStore::new(cx)); + + cx.observe_new_views(move |workspace: &mut Workspace, cx| { + let project = workspace.project(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(project, cx); + }); + } + + let log_store = log_store.clone(); + workspace.register_action(move |workspace, _: &OpenDebuggerServerLogs, cx| { + let project = workspace.project().read(cx); + if project.is_local() { + workspace.add_item_to_active_pane( + Box::new(cx.new_view(|cx| { + DapLogView::new(workspace.project().clone(), log_store.clone(), cx) + })), + None, + true, + cx, + ); + } + }); + }) + .detach(); +} + +impl Item for DapLogView { + type Event = EditorEvent; + + fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some("DAP Logs".into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } +} + +impl SearchableItem for DapLogView { + type Match = ::Match; + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |e, cx| e.clear_matches(cx)) + } + + fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.update_matches(matches, cx)) + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.editor.update(cx, |e, cx| e.query_suggestion(cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: &[Self::Match], + cx: &mut ViewContext, + ) { + self.editor + .update(cx, |e, cx| e.activate_match(index, matches, cx)) + } + + fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |e, cx| e.find_matches(query, cx)) + } + + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since DAP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // DAP log is read-only. + replacement: false, + selection: false, + } + } + fn active_match_index( + &mut self, + matches: &[Self::Match], + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |e, cx| e.active_match_index(matches, cx)) + } +} + +impl FocusableView for DapLogView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +pub enum Event { + NewLogEntry { + id: DebugAdapterClientId, + entry: String, + kind: LogKind, + }, +} + +impl EventEmitter for LogStore {} +impl EventEmitter for DapLogView {} +impl EventEmitter for DapLogView {} +impl EventEmitter for DapLogView {} diff --git a/crates/debugger_tools/src/debugger_tools.rs b/crates/debugger_tools/src/debugger_tools.rs new file mode 100644 index 00000000000000..744e90669bf402 --- /dev/null +++ b/crates/debugger_tools/src/debugger_tools.rs @@ -0,0 +1,8 @@ +mod dap_log; +pub use dap_log::*; + +use gpui::AppContext; + +pub fn init(cx: &mut AppContext) { + dap_log::init(cx); +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d69146fa90f92f..30e1d415139f29 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -241,11 +241,13 @@ pub enum Event { }, LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), + DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, message: Message, }, + DebugClientLog(DebugAdapterClientId, String), ActiveEntryChanged(Option), ActivateProjectPanel, WorktreeAdded, @@ -2341,6 +2343,7 @@ impl Project { self.dap_store.update(cx, |store, cx| { store.initialize(client_id, cx).detach_and_log_err(cx) }); + cx.emit(Event::DebugClientStarted(*client_id)); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); @@ -4292,6 +4295,21 @@ impl Project { .read(cx) .language_servers_for_buffer(buffer, cx) } + + pub fn debug_clients<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.dap_store.read(cx).running_clients() + } + + pub fn debug_client_for_id( + &self, + id: &DebugAdapterClientId, + cx: &AppContext, + ) -> Option> { + self.dap_store.read(cx).client_by_id(id) + } } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2f3b79294d5479..7596d07c58d0bf 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,6 +36,7 @@ command_palette.workspace = true command_palette_hooks.workspace = true copilot.workspace = true debugger_ui.workspace = true +debugger_tools.workspace = true db.workspace = true dev_server_projects.workspace = true diagnostics.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ba445a5d37ade6..eea061f12f7fc0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -523,6 +523,7 @@ fn main() { zed::init(cx); project::Project::init(&client, cx); debugger_ui::init(cx); + debugger_tools::init(cx); client::init(&client, cx); language::init(cx); let telemetry = client.telemetry(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2303ec35832f38..778e7042f87cb0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -598,6 +598,8 @@ fn initialize_pane(workspace: &Workspace, pane: &View, cx: &mut ViewContex toolbar.add_item(project_search_bar, cx); let lsp_log_item = cx.new_view(|_| language_tools::LspLogToolbarItemView::new()); toolbar.add_item(lsp_log_item, cx); + let dap_log_item = cx.new_view(|_| debugger_tools::DapLogToolbarItemView::new()); + toolbar.add_item(dap_log_item, cx); let syntax_tree_item = cx.new_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); toolbar.add_item(syntax_tree_item, cx);