diff --git a/Cargo.lock b/Cargo.lock index 1cda382d803d10..6f2cd7a1253083 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", ] @@ -3463,6 +3465,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" @@ -14691,6 +14707,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);