From 4ee71ade4390b51447768a082264641c9eba504f Mon Sep 17 00:00:00 2001 From: Borna Butkovic Date: Wed, 25 Sep 2024 00:18:48 +0200 Subject: [PATCH 1/8] Implement RPC logging for debug adapter clients --- Cargo.lock | 28 +- Cargo.toml | 2 + crates/dap/src/client.rs | 23 +- crates/dap/src/transport.rs | 51 +- crates/debugger_tools/Cargo.toml | 24 + crates/debugger_tools/TODO.md | 4 + crates/debugger_tools/src/dap_log.rs | 776 ++++++++++++++++++++ crates/debugger_tools/src/debugger_tools.rs | 8 + crates/project/src/project.rs | 21 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 + 12 files changed, 932 insertions(+), 9 deletions(-) create mode 100644 crates/debugger_tools/Cargo.toml create mode 100644 crates/debugger_tools/TODO.md 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 9973b2ece813d6..8dfe40427f68cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3346,7 +3346,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "dap-types", + "dap-types 0.0.1 (git+https://github.com/zed-industries/dap-types?rev=b95818130022bfc72bbcd639bdd0c0358c7549fc)", "fs", "futures 0.3.30", "gpui", @@ -3372,6 +3372,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "dap-types" +version = "0.0.1" +source = "git+https://github.com/zed-industries/dap-types#b95818130022bfc72bbcd639bdd0c0358c7549fc" +dependencies = [ + "schemars", + "serde", + "serde_json", +] + [[package]] name = "dap_adapters" version = "0.1.0" @@ -3463,6 +3473,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "debugger_tools" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap", + "dap-types 0.0.1 (git+https://github.com/zed-industries/dap-types)", + "editor", + "futures 0.3.30", + "gpui", + "project", + "serde_json", + "workspace", +] + [[package]] name = "debugger_ui" version = "0.1.0" @@ -14684,6 +14709,7 @@ dependencies = [ "command_palette_hooks", "copilot", "db", + "debugger_tools", "debugger_ui", "dev_server_projects", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 35663baf944a69..2d4388d8694ee6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -174,6 +174,7 @@ members = [ # "tooling/xtask", + "crates/debugger_tools", ] default-members = ["crates/zed"] @@ -209,6 +210,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/src/client.rs b/crates/dap/src/client.rs index e484d1516179fe..81ecb4dd866dbe 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,5 +1,8 @@ -use crate::transport::Transport; -use anyhow::{anyhow, Result}; +pub use crate::transport::IoKind; +use crate::transport::{IoHandler, Transport}; +use anyhow::{anyhow, Context, Result}; + +use crate::adapters::DebugAdapter; use dap_types::{ messages::{Message, Response}, requests::Request, @@ -42,6 +45,7 @@ pub struct DebugAdapterClient { _process: Arc>>, sequence_count: AtomicU64, config: DebugAdapterConfig, + io_handlers: Arc>>, } pub struct TransportParams { @@ -80,11 +84,14 @@ impl DebugAdapterClient { where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { + let io_handlers = Arc::new(Mutex::new(Vec::new())); + let transport = Self::handle_transport( transport_params.rx, transport_params.tx, transport_params.err, event_handler, + io_handlers.clone(), cx, ); Ok(Arc::new(Self { @@ -93,6 +100,7 @@ impl DebugAdapterClient { request_args, config, transport, + io_handlers, sequence_count: AtomicU64::new(1), _process: Arc::new(Mutex::new(transport_params.process)), })) @@ -103,12 +111,13 @@ impl DebugAdapterClient { tx: Box, err: Option>, event_handler: F, + io_handlers: Arc>>, cx: &mut AsyncAppContext, ) -> Arc where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - let transport = Transport::start(rx, tx, err, cx); + let transport = Transport::start(rx, tx, err, io_handlers, cx); let server_rx = transport.server_rx.clone(); let server_tr = transport.server_tx.clone(); @@ -233,4 +242,12 @@ impl DebugAdapterClient { } .await } + + pub fn on_io(&self, f: F) + where + F: 'static + Send + FnMut(IoKind, &str), + { + let mut io_handlers = self.io_handlers.lock(); + io_handlers.push(Box::new(f)); + } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 5a7dbbd4ff3135..bcef01d9f62f03 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -12,6 +12,15 @@ use smol::{ }; use std::{collections::HashMap, sync::Arc}; +pub type IoHandler = Box; + +#[derive(Debug, Clone, Copy)] +pub enum IoKind { + StdIn, + StdOut, + StdErr, +} + #[derive(Debug)] pub struct Transport { pub server_tx: Sender, @@ -25,6 +34,7 @@ impl Transport { server_stdout: Box, server_stdin: Box, server_stderr: Option>, + io_handlers: Arc>>, cx: &mut AsyncAppContext, ) -> Arc { let (client_tx, server_rx) = unbounded::(); @@ -38,11 +48,14 @@ impl Transport { pending_requests.clone(), server_stdout, client_tx, + io_handlers.clone(), )) .detach(); if let Some(stderr) = server_stderr { - cx.background_executor().spawn(Self::err(stderr)).detach(); + cx.background_executor() + .spawn(Self::err(stderr, io_handlers.clone())) + .detach(); } cx.background_executor() @@ -51,6 +64,7 @@ impl Transport { pending_requests.clone(), server_stdin, client_rx, + io_handlers, )) .detach(); @@ -65,6 +79,7 @@ impl Transport { async fn recv_server_message( reader: &mut Box, buffer: &mut String, + io_handlers: Arc>>, ) -> Result { let mut content_length = None; loop { @@ -102,18 +117,27 @@ impl Transport { .with_context(|| "reading after a loop")?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; + + for handler in io_handlers.lock().iter_mut() { + handler(IoKind::StdOut, msg); + } Ok(serde_json::from_str::(msg)?) } async fn recv_server_error( err: &mut (impl AsyncBufRead + Unpin + Send), buffer: &mut String, + io_handlers: Arc>>, ) -> Result<()> { buffer.truncate(0); if err.read_line(buffer).await? == 0 { return Err(anyhow!("debugger error stream closed")); }; + for handler in io_handlers.lock().iter_mut() { + handler(IoKind::StdErr, buffer.as_str()); + } + Ok(()) } @@ -122,23 +146,30 @@ impl Transport { pending_requests: &Mutex>>>, server_stdin: &mut Box, mut payload: Message, + io_handlers: Arc>>, ) -> Result<()> { if let Message::Request(request) = &mut payload { if let Some(sender) = current_requests.lock().await.remove(&request.seq) { pending_requests.lock().await.insert(request.seq, sender); } } - Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await + Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?, io_handlers) + .await } async fn send_string_to_server( server_stdin: &mut Box, request: String, + io_handlers: Arc>>, ) -> Result<()> { server_stdin .write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes()) .await?; + for handler in io_handlers.lock().iter_mut() { + handler(IoKind::StdIn, request.as_str()); + } + server_stdin.flush().await?; Ok(()) } @@ -186,10 +217,14 @@ impl Transport { pending_requests: Arc>>>>, mut server_stdout: Box, client_tx: Sender, + io_handlers: Arc>>, ) -> Result<()> { let mut recv_buffer = String::new(); - while let Ok(msg) = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { + while let Ok(msg) = + Self::recv_server_message(&mut server_stdout, &mut recv_buffer, io_handlers.clone()) + .await + { Self::process_server_message(&pending_requests, &client_tx, msg) .await .context("Process server message failed in transport::receive")?; @@ -203,6 +238,7 @@ impl Transport { pending_requests: Arc>>>>, mut server_stdin: Box, client_rx: Receiver, + io_handlers: Arc>>, ) -> Result<()> { while let Ok(payload) = client_rx.recv().await { Self::send_payload_to_server( @@ -210,6 +246,7 @@ impl Transport { &pending_requests, &mut server_stdin, payload, + io_handlers.clone(), ) .await?; } @@ -217,10 +254,14 @@ impl Transport { Ok(()) } - async fn err(mut server_stderr: Box) -> Result<()> { + async fn err( + mut server_stderr: Box, + io_handlers: Arc>>, + ) -> Result<()> { let mut recv_buffer = String::new(); loop { - Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?; + Self::recv_server_error(&mut server_stderr, &mut recv_buffer, io_handlers.clone()) + .await?; } } } diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml new file mode 100644 index 00000000000000..f3784ea3bfcb4e --- /dev/null +++ b/crates/debugger_tools/Cargo.toml @@ -0,0 +1,24 @@ +[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 +dap-types = { git = "https://github.com/zed-industries/dap-types" } +serde_json.workspace = true +futures.workspace = true +anyhow.workspace = true diff --git a/crates/debugger_tools/TODO.md b/crates/debugger_tools/TODO.md new file mode 100644 index 00000000000000..a29f833127e85f --- /dev/null +++ b/crates/debugger_tools/TODO.md @@ -0,0 +1,4 @@ +- [ ] housekeeping: ensure references to debug adapters are cleaned when they are closed +- [ ] feature: log server messages when available? +- [ ] improve the client menu +- [ ] log events separately? diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs new file mode 100644 index 00000000000000..93b45a1f59fb4a --- /dev/null +++ b/crates/debugger_tools/src/dap_log.rs @@ -0,0 +1,776 @@ +use dap::client::IoKind; +use dap::{ + adapters::DebugAdapterName, + client::{DebugAdapterClient, DebugAdapterClientId}, +}; +use editor::{Editor, EditorEvent}; +use futures::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, Checkbox, Clickable, ContextMenu, Label, PopoverMenu, Selection}, + ToolbarItemEvent, ToolbarItemView, Workspace, +}; + +struct DapLogView { + pub(crate) 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, + io_tx: futures::channel::mpsc::UnboundedSender<(DebugAdapterClientId, IoKind, String)>, +} + +struct ProjectState { + _subscriptions: [gpui::Subscription; 2], +} + +struct DebugAdapterState { + // log_messages: VecDeque, + rpc_messages: Option, +} + +struct RpcMessages { + messages: VecDeque, + last_message_kind: Option, +} + +impl RpcMessages { + fn new() -> Self { + Self { + messages: VecDeque::with_capacity(255), + last_message_kind: None, + } + } +} + +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: None, + } + } +} + +impl LogStore { + fn new(cx: &ModelContext) -> Self { + let (io_tx, mut io_rx) = + futures::channel::mpsc::unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = io_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_io(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + Self { + projects: HashMap::new(), + debug_clients: HashMap::new(), + io_tx, + } + } + + fn on_io( + &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(()) + } + + 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(id) => { + let read_project = project.read(cx); + if let Some(client) = read_project.debug_client_for_id(*id, cx) { + this.add_debug_client(client.id(), Some(client), 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, + ) -> Option<()> { + let debug_client_state = self.get_debug_adapter_state(id)?; + + 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 let Some(rpc_messages) = rpc_messages { + if rpc_messages.last_message_kind != Some(kind) { + Self::add_debug_client_log( + &mut rpc_messages.messages, + id, + kind.label().to_string(), + LogKind::Rpc, + cx, + ); + rpc_messages.last_message_kind = Some(kind); + } + Self::add_debug_client_log(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); + } + + Some(()) + } + + fn add_debug_client_log( + log_lines: &mut VecDeque, + id: DebugAdapterClientId, + message: String, + kind: LogKind, + cx: &mut ModelContext, + ) { + while log_lines.len() >= 255 { + 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>, + cx: &mut ModelContext, + ) -> Option<&mut DebugAdapterState> { + let client_state = self.debug_clients.entry(client_id).or_insert_with(|| { + cx.notify(); + DebugAdapterState::new() + }); + + if let Some(client) = client { + let io_tx = self.io_tx.clone(); + client.on_io(move |io_kind, message| { + io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }); + } + + Some(client_state) + } + + fn remove_debug_client(&mut self, id: DebugAdapterClientId, cx: &mut ModelContext) { + self.debug_clients.remove(&id); + cx.notify(); + } + + fn rpc_logging_enabled_for_client(&self, client_id: DebugAdapterClientId) -> bool { + self.debug_clients + .get(&client_id) + .is_some_and(|client| client.rpc_messages.is_some()) + } + + fn enable_rpc_trace_for_debug_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + let rpc_state = self + .debug_clients + .get_mut(&client_id)? + .rpc_messages + .get_or_insert_with(|| RpcMessages::new()); + Some(&mut rpc_state.messages) + } + + pub fn disable_rpc_trace_for_debug_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<()> { + self.debug_clients.get_mut(&client_id)?.rpc_messages.take(); + Some(()) + } + + // fn clients(&self) -> impl Iterator { + // self.debug_clients.iter() + // } +} + +pub struct DapLogToolbarItemView { + log_view: Option>, +} + +impl DapLogToolbarItemView { + pub fn new() -> Self { + Self { log_view: None } + } + + fn toggle_rpc_logging_for_client( + &mut self, + id: DebugAdapterClientId, + enabled: bool, + cx: &mut ViewContext, + ) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |log_view, cx| { + log_view.toggle_rpc_trace_for_server(id, enabled, cx); + if enabled { + log_view.show_rpc_trace_for_server(id, cx); + cx.notify(); + } + cx.focus(&log_view.focus_handle); + }); + } + cx.notify(); + } +} + +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].clone()) + } else { + None + } + }); + + let log_toolbar_view = cx.view().clone(); + 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, + row.worktree_root_name, + row.selected_entry.label() + )) + }) + .unwrap_or_else(|| "No server selected".into()), + )) + .menu({ + let _log_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + let log_view = log_view.clone(); + let menu_rows = menu_rows.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for (ix, row) in menu_rows.into_iter().enumerate() { + // let server_selected = Some(row.client_id) == current_client_id; + + // menu = menu + // .header(format!( + // "{} ({})", + // row.client_name.0, row.worktree_root_name + // )) + // .entry( + // CLIENT_LOGS, + // None, + // cx.handler_for(&log_view, move |view, cx| { + // // view show_logs_for_server(row.server_id, cx); + // }), + // ); + menu = menu.custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().child( + Checkbox::new( + ix, + if row.rpc_trace_enabled { + Selection::Selected + } else { + Selection::Unselected + }, + ) + .on_click(cx.listener_for( + &log_toolbar_view, + move |view, selection, cx| { + let enabled = matches!( + selection, + Selection::Selected + ); + view.toggle_rpc_logging_for_client( + row.client_id, + enabled, + cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .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, // TODO + ) -> 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; + 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<'a>(&self, cx: &'a AppContext) -> Option> { + let log_store = self.log_store.read(cx); + + let mut rows = self + .project + .read(cx) + .debug_clients(cx) + .filter_map(|client| { + Some(DapMenuItem { + client_id: client.id(), + client_name: "TODO".into(), + worktree_root_name: String::new(), + selected_entry: LogKind::Rpc, + rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), + }) + }) + .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 + .enable_rpc_trace_for_debug_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 toggle_rpc_trace_for_server( + &mut self, + client_id: DebugAdapterClientId, + enabled: bool, + cx: &mut ViewContext, + ) { + self.log_store.update(cx, |log_store, _| { + if enabled { + println!("rpc enabled"); + log_store.enable_rpc_trace_for_debug_client(client_id); + } else { + println!("rpc disabled"); + log_store.disable_rpc_trace_for_debug_client(client_id); + } + }); + if !enabled && Some(client_id) == self.current_view.map(|(client_id, _)| client_id) { + // self.show(client_id, cx); + cx.notify(); + } + } +} + +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, Debug, PartialEq)] +pub(crate) struct DapMenuItem { + pub client_id: DebugAdapterClientId, + pub client_name: String, + pub worktree_root_name: String, + pub rpc_trace_enabled: bool, + pub selected_entry: LogKind, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum LogKind { + Rpc, + #[default] + Logs, +} + +const CLIENT_LOGS: &str = "Client Logs"; +const RPC_MESSAGES: &str = "RPC Messages"; + +impl LogKind { + fn label(&self) -> &'static str { + match self { + LogKind::Rpc => RPC_MESSAGES, + LogKind::Logs => CLIENT_LOGS, + } + } +} + +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 d0d337346ae344..ea9afef283dd11 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, @@ -2337,6 +2339,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)); @@ -4246,6 +4249,8 @@ impl Project { Ok(()) } + + pub fn language_servers<'a>( &'a self, cx: &'a AppContext, @@ -4277,6 +4282,22 @@ 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 b784e1bc30c479..de07ae2e2f136f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -599,6 +599,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); From 28c6ccee065522fff400a20b6ee1bf007eca0084 Mon Sep 17 00:00:00 2001 From: Borna Butkovic Date: Sat, 19 Oct 2024 01:48:02 +0200 Subject: [PATCH 2/8] Implement server logs for debugger servers --- crates/dap/src/adapters.rs | 38 ++++-- crates/dap/src/client.rs | 77 +++++++++++- crates/dap_adapters/src/custom.rs | 10 +- crates/dap_adapters/src/dap_adapters.rs | 2 +- crates/dap_adapters/src/javascript.rs | 6 +- crates/dap_adapters/src/lldb.rs | 4 +- crates/dap_adapters/src/php.rs | 6 +- crates/dap_adapters/src/python.rs | 4 +- crates/debugger_tools/TODO.md | 3 - crates/debugger_tools/src/dap_log.rs | 149 +++++++++++++++++++----- crates/project/src/dap_store.rs | 4 +- 11 files changed, 243 insertions(+), 60 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 236297eba01510..b37159cf82f2f6 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,4 +1,3 @@ -use crate::client::TransportParams; use ::fs::Fs; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -26,6 +25,8 @@ use std::{ use task::{DebugAdapterConfig, TCPHost}; +use crate::client::{AdapterLogIo, TransportParams}; + /// Get an open port to use with the tcp client when not supplied by debug config async fn get_open_port(host: Ipv4Addr) -> Option { Some( @@ -53,7 +54,7 @@ pub async fn create_tcp_client( host: TCPHost, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, -) -> Result { +) -> Result<(TransportParams, AdapterLogIo)> { let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); let mut port = host.port; @@ -73,14 +74,23 @@ pub async fn create_tcp_client( command .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .kill_on_drop(true); - let process = command + let mut process = command .spawn() .with_context(|| "failed to start debug adapter.")?; + let log_stdout = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let log_stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + if let Some(delay) = host.delay { // some debug adapters need some time to start the TCP server // so we have to wait few milliseconds before we can connect to it @@ -97,11 +107,17 @@ pub async fn create_tcp_client( let (rx, tx) = TcpStream::connect(address).await?.split(); log::info!("Debug adapter has connected to tcp server"); - Ok(TransportParams::new( - Box::new(BufReader::new(rx)), - Box::new(tx), - None, - Some(process), + Ok(( + TransportParams::new( + Box::new(BufReader::new(rx)), + Box::new(tx), + None, + Some(process), + ), + AdapterLogIo::new( + Box::new(BufReader::new(log_stdout)), + Box::new(BufReader::new(log_stderr)), + ), )) } @@ -186,7 +202,7 @@ pub trait DebugAdapter: 'static + Send + Sync { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> anyhow::Result; + ) -> anyhow::Result<(TransportParams, Option)>; /// Installs the binary for the debug adapter. /// This method is called when the adapter binary is not found or needs to be updated. diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 81ecb4dd866dbe..4bdc28b3860c12 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,13 +1,12 @@ pub use crate::transport::IoKind; use crate::transport::{IoHandler, Transport}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; -use crate::adapters::DebugAdapter; use dap_types::{ messages::{Message, Response}, requests::Request, }; -use futures::{AsyncBufRead, AsyncWrite}; +use futures::{AsyncBufRead, AsyncBufReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; use parking_lot::Mutex; use serde_json::Value; @@ -46,6 +45,7 @@ pub struct DebugAdapterClient { sequence_count: AtomicU64, config: DebugAdapterConfig, io_handlers: Arc>>, + log_io_handlers: Option>>>, } pub struct TransportParams { @@ -71,6 +71,20 @@ impl TransportParams { } } +pub struct AdapterLogIo { + out: Box, + err: Box, +} + +impl AdapterLogIo { + pub fn new( + out: Box, + err: Box, + ) -> Self { + Self { out, err } + } +} + impl DebugAdapterClient { pub async fn new( id: DebugAdapterClientId, @@ -78,6 +92,7 @@ impl DebugAdapterClient { request_args: Value, config: DebugAdapterConfig, transport_params: TransportParams, + adapter_log_io: Option, event_handler: F, cx: &mut AsyncAppContext, ) -> Result> @@ -85,6 +100,15 @@ impl DebugAdapterClient { F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { let io_handlers = Arc::new(Mutex::new(Vec::new())); + let log_io_handlers = match adapter_log_io { + Some(AdapterLogIo { out, err }) => { + let log_io_handlers = Arc::new(Mutex::new(Vec::new())); + + Self::handle_adapter_logs(out, err, log_io_handlers.clone(), cx); + Some(log_io_handlers) + } + None => None, + }; let transport = Self::handle_transport( transport_params.rx, @@ -101,6 +125,7 @@ impl DebugAdapterClient { config, transport, io_handlers, + log_io_handlers, sequence_count: AtomicU64::new(1), _process: Arc::new(Mutex::new(transport_params.process)), })) @@ -129,6 +154,38 @@ impl DebugAdapterClient { transport } + fn handle_adapter_logs( + out: Box, + err: Box, + log_io_handlers: Arc>>, + cx: &mut AsyncAppContext, + ) { + { + let mut out = out.lines(); + let log_io_handlers = log_io_handlers.clone(); + cx.spawn(|_| async move { + use futures::stream::StreamExt; + while let Some(Ok(line)) = out.next().await { + for handler in log_io_handlers.lock().iter_mut() { + handler(IoKind::StdOut, line.as_ref()); + } + } + }) + .detach(); + } + + let mut err = err.lines(); + cx.spawn(|_| async move { + use futures::stream::StreamExt; + while let Some(Ok(line)) = err.next().await { + for handler in log_io_handlers.lock().iter_mut() { + handler(IoKind::StdErr, line.as_ref()); + } + } + }) + .detach(); + } + async fn handle_recv( server_rx: Receiver, client_tx: Sender, @@ -192,6 +249,10 @@ impl DebugAdapterClient { .map_err(|e| anyhow::anyhow!("Failed to send response back: {}", e)) } + pub fn has_adapter_logs(&self) -> bool { + self.log_io_handlers.is_some() + } + pub fn id(&self) -> DebugAdapterClientId { self.id } @@ -250,4 +311,14 @@ impl DebugAdapterClient { let mut io_handlers = self.io_handlers.lock(); io_handlers.push(Box::new(f)); } + + pub fn on_log_io(&self, f: F) + where + F: 'static + Send + FnMut(IoKind, &str), + { + if let Some(ref io_log_handlers) = self.log_io_handlers { + let mut io_log_handlers = io_log_handlers.lock(); + io_log_handlers.push(Box::new(f)); + } + } } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index a33e1802839b5b..b2accf045be59c 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -28,11 +28,15 @@ impl DebugAdapter for CustomDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result<(TransportParams, Option)> { match &self.custom_args.connection { - DebugConnectionType::STDIO => create_stdio_client(adapter_binary), + DebugConnectionType::STDIO => { + create_stdio_client(adapter_binary).map(|transport| (transport, None)) + } DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), adapter_binary, cx).await + create_tcp_client(tcp_host.clone(), adapter_binary, cx) + .await + .map(|(transport, log_io)| (transport, Some(log_io))) } } } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index fac4f3c77c7377..9f405ee989692d 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -17,7 +17,7 @@ use dap::{ create_stdio_client, create_tcp_client, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, }, - client::TransportParams, + client::{AdapterLogIo, TransportParams}, }; use gpui::AsyncAppContext; use http_client::github::latest_github_release; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 185a785a6ff4cb..6d23722e65c8f0 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -22,14 +22,16 @@ impl DebugAdapter for JsDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result<(TransportParams, Option)> { let host = TCPHost { port: Some(8133), host: None, delay: Some(1000), }; - create_tcp_client(host, adapter_binary, cx).await + create_tcp_client(host, adapter_binary, cx) + .await + .map(|(transport, log_io)| (transport, Some(log_io))) } async fn fetch_binary( diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 56cda6750a2752..0f8630a73f54c7 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -25,8 +25,8 @@ impl DebugAdapter for LldbDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, _: &mut AsyncAppContext, - ) -> Result { - create_stdio_client(adapter_binary) + ) -> Result<(TransportParams, Option)> { + create_stdio_client(adapter_binary).map(|transport| (transport, None)) } async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 62ecb16506bb82..4d1be30132b294 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -22,14 +22,16 @@ impl DebugAdapter for PhpDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result<(TransportParams, Option)> { let host = TCPHost { port: Some(8132), host: None, delay: Some(1000), }; - create_tcp_client(host, adapter_binary, cx).await + create_tcp_client(host, adapter_binary, cx) + .await + .map(|(transport, log_io)| (transport, Some(log_io))) } async fn fetch_binary( diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 12b16280f0de08..73fdec036f5604 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -22,8 +22,8 @@ impl DebugAdapter for PythonDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, _: &mut AsyncAppContext, - ) -> Result { - create_stdio_client(adapter_binary) + ) -> Result<(TransportParams, Option)> { + create_stdio_client(adapter_binary).map(|transport| (transport, None)) } async fn fetch_binary( diff --git a/crates/debugger_tools/TODO.md b/crates/debugger_tools/TODO.md index a29f833127e85f..df0cae0b4b6168 100644 --- a/crates/debugger_tools/TODO.md +++ b/crates/debugger_tools/TODO.md @@ -1,4 +1 @@ - [ ] housekeeping: ensure references to debug adapters are cleaned when they are closed -- [ ] feature: log server messages when available? -- [ ] improve the client menu -- [ ] log events separately? diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 93b45a1f59fb4a..97f7c7a48848d2 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,8 +1,5 @@ use dap::client::IoKind; -use dap::{ - adapters::DebugAdapterName, - client::{DebugAdapterClient, DebugAdapterClientId}, -}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use editor::{Editor, EditorEvent}; use futures::StreamExt; use gpui::{ @@ -37,6 +34,7 @@ struct LogStore { projects: HashMap, ProjectState>, debug_clients: HashMap, io_tx: futures::channel::mpsc::UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + log_io_tx: futures::channel::mpsc::UnboundedSender<(DebugAdapterClientId, IoKind, String)>, } struct ProjectState { @@ -44,7 +42,7 @@ struct ProjectState { } struct DebugAdapterState { - // log_messages: VecDeque, + log_messages: VecDeque, rpc_messages: Option, } @@ -83,7 +81,7 @@ impl MessageKind { impl DebugAdapterState { fn new() -> Self { Self { - // log_messages: VecDeque::new(), + log_messages: VecDeque::new(), rpc_messages: None, } } @@ -104,10 +102,25 @@ impl LogStore { anyhow::Ok(()) }) .detach_and_log_err(cx); + + let (log_io_tx, mut log_io_rx) = + futures::channel::mpsc::unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = log_io_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_log_io(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); Self { projects: HashMap::new(), debug_clients: HashMap::new(), io_tx, + log_io_tx, } } @@ -123,6 +136,18 @@ impl LogStore { Some(()) } + fn on_log_io( + &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( @@ -174,7 +199,7 @@ impl LogStore { let rpc_messages = &mut debug_client_state.rpc_messages; if let Some(rpc_messages) = rpc_messages { if rpc_messages.last_message_kind != Some(kind) { - Self::add_debug_client_log( + Self::add_debug_client_entry( &mut rpc_messages.messages, id, kind.label().to_string(), @@ -183,13 +208,38 @@ impl LogStore { ); rpc_messages.last_message_kind = Some(kind); } - Self::add_debug_client_log(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); + Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); } Some(()) } fn add_debug_client_log( + &mut self, + id: DebugAdapterClientId, + io_kind: IoKind, + message: String, + cx: &mut ModelContext, + ) -> Option<()> { + let debug_client_state = self.get_debug_adapter_state(id)?; + + 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::Logs, cx); + + Some(()) + } + + fn add_debug_client_entry( log_lines: &mut VecDeque, id: DebugAdapterClientId, message: String, @@ -225,6 +275,15 @@ impl LogStore { .unbounded_send((client_id, io_kind, message.to_string())) .ok(); }); + + if client.has_adapter_logs() { + let log_io_tx = self.log_io_tx.clone(); + client.on_log_io(move |io_kind, message| { + log_io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }); + } } Some(client_state) @@ -253,6 +312,13 @@ impl LogStore { Some(&mut rpc_state.messages) } + fn log_messages_for_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) + } + pub fn disable_rpc_trace_for_debug_client( &mut self, client_id: DebugAdapterClientId, @@ -324,9 +390,8 @@ impl Render for DapLogToolbarItemView { current_client .map(|row| { Cow::Owned(format!( - "{} ({}) - {}", + "{} - {}", row.client_name, - row.worktree_root_name, row.selected_entry.label() )) }) @@ -341,20 +406,18 @@ impl Render for DapLogToolbarItemView { let log_toolbar_view = log_toolbar_view.clone(); ContextMenu::build(cx, move |mut menu, cx| { for (ix, row) in menu_rows.into_iter().enumerate() { - // let server_selected = Some(row.client_id) == current_client_id; - - // menu = menu - // .header(format!( - // "{} ({})", - // row.client_name.0, row.worktree_root_name - // )) - // .entry( - // CLIENT_LOGS, - // None, - // cx.handler_for(&log_view, move |view, cx| { - // // view show_logs_for_server(row.server_id, cx); - // }), - // ); + menu = menu.header(format!("{}", row.client_name,)); + + if row.has_adapter_logs { + menu = menu.entry( + CLIENT_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_log_messages_for_server(row.client_id, cx); + }), + ); + } + menu = menu.custom_entry( { let log_toolbar_view = log_toolbar_view.clone(); @@ -522,9 +585,9 @@ impl DapLogView { .filter_map(|client| { Some(DapMenuItem { client_id: client.id(), - client_name: "TODO".into(), - worktree_root_name: String::new(), - selected_entry: LogKind::Rpc, + client_name: client.config().kind.to_string(), + selected_entry: self.current_view.map_or(LogKind::Logs, |(_, kind)| kind), + has_adapter_logs: client.has_adapter_logs(), rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), }) }) @@ -575,6 +638,34 @@ impl DapLogView { 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::Logs)); + 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 toggle_rpc_trace_for_server( &mut self, client_id: DebugAdapterClientId, @@ -583,10 +674,8 @@ impl DapLogView { ) { self.log_store.update(cx, |log_store, _| { if enabled { - println!("rpc enabled"); log_store.enable_rpc_trace_for_debug_client(client_id); } else { - println!("rpc disabled"); log_store.disable_rpc_trace_for_debug_client(client_id); } }); @@ -612,7 +701,7 @@ fn log_contents(lines: &VecDeque) -> String { pub(crate) struct DapMenuItem { pub client_id: DebugAdapterClientId, pub client_name: String, - pub worktree_root_name: String, + pub has_adapter_logs: bool, pub rpc_trace_enabled: bool, pub selected_entry: LogKind, } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 12715a96644cec..a22f05734a129a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -273,7 +273,8 @@ impl DapStore { merge_json_value_into(args.configuration, &mut request_args); } - let transport_params = adapter.connect(&binary, &mut cx).await.log_err()?; + let (transport_params, adapter_log_io) = + adapter.connect(&binary, &mut cx).await.log_err()?; let client = DebugAdapterClient::new( client_id, @@ -281,6 +282,7 @@ impl DapStore { request_args, config, transport_params, + adapter_log_io, move |message, cx| { dap_store .update(cx, |_, cx| { From b8e1ae28fd2ea4f08e93277789577014137043c4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 20 Oct 2024 12:19:19 +0200 Subject: [PATCH 3/8] 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 --- Cargo.lock | 13 +-- crates/dap/src/adapters.rs | 36 ++++--- crates/dap/src/client.rs | 120 ++++++++++-------------- crates/dap/src/transport.rs | 13 ++- crates/dap_adapters/src/custom.rs | 10 +- crates/dap_adapters/src/dap_adapters.rs | 2 +- crates/dap_adapters/src/javascript.rs | 6 +- crates/dap_adapters/src/lldb.rs | 4 +- crates/dap_adapters/src/php.rs | 6 +- crates/dap_adapters/src/python.rs | 4 +- crates/debugger_tools/Cargo.toml | 1 - crates/debugger_tools/TODO.md | 1 - crates/debugger_tools/src/dap_log.rs | 89 ++++++++---------- crates/project/src/dap_store.rs | 4 +- crates/project/src/project.rs | 7 +- 15 files changed, 127 insertions(+), 189 deletions(-) delete mode 100644 crates/debugger_tools/TODO.md diff --git a/Cargo.lock b/Cargo.lock index 8dfe40427f68cc..29230a380011d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3346,7 +3346,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "dap-types 0.0.1 (git+https://github.com/zed-industries/dap-types?rev=b95818130022bfc72bbcd639bdd0c0358c7549fc)", + "dap-types", "fs", "futures 0.3.30", "gpui", @@ -3372,16 +3372,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "dap-types" -version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#b95818130022bfc72bbcd639bdd0c0358c7549fc" -dependencies = [ - "schemars", - "serde", - "serde_json", -] - [[package]] name = "dap_adapters" version = "0.1.0" @@ -3479,7 +3469,6 @@ version = "0.1.0" dependencies = [ "anyhow", "dap", - "dap-types 0.0.1 (git+https://github.com/zed-industries/dap-types)", "editor", "futures 0.3.30", "gpui", diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index b37159cf82f2f6..cba49db251b930 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -25,7 +25,7 @@ use std::{ use task::{DebugAdapterConfig, TCPHost}; -use crate::client::{AdapterLogIo, TransportParams}; +use crate::client::TransportParams; /// Get an open port to use with the tcp client when not supplied by debug config async fn get_open_port(host: Ipv4Addr) -> Option { @@ -54,7 +54,7 @@ pub async fn create_tcp_client( host: TCPHost, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, -) -> Result<(TransportParams, AdapterLogIo)> { +) -> Result { let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); let mut port = host.port; @@ -82,11 +82,11 @@ pub async fn create_tcp_client( .spawn() .with_context(|| "failed to start debug adapter.")?; - let log_stdout = process + let stdout = process .stdout .take() .ok_or_else(|| anyhow!("Failed to open stdout"))?; - let log_stderr = process + let stderr = process .stderr .take() .ok_or_else(|| anyhow!("Failed to open stderr"))?; @@ -104,20 +104,15 @@ pub async fn create_tcp_client( port.ok_or(anyhow!("Port is required to connect to TCP server"))?, ); - let (rx, tx) = TcpStream::connect(address).await?.split(); + let (output, input) = TcpStream::connect(address).await?.split(); log::info!("Debug adapter has connected to tcp server"); - Ok(( - TransportParams::new( - Box::new(BufReader::new(rx)), - Box::new(tx), - None, - Some(process), - ), - AdapterLogIo::new( - Box::new(BufReader::new(log_stdout)), - Box::new(BufReader::new(log_stderr)), - ), + Ok(TransportParams::new( + Box::new(input), + Box::new(BufReader::new(output)), + Some(Box::new(BufReader::new(stdout))), + Box::new(BufReader::new(stderr)), + process, )) } @@ -162,10 +157,11 @@ pub fn create_stdio_client(adapter_binary: &DebugAdapterBinary) -> Result anyhow::Result<(TransportParams, Option)>; + ) -> anyhow::Result; /// Installs the binary for the debug adapter. /// This method is called when the adapter binary is not found or needs to be updated. diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 4bdc28b3860c12..d5cf75cbaad9a0 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -6,7 +6,7 @@ use dap_types::{ messages::{Message, Response}, requests::Request, }; -use futures::{AsyncBufRead, AsyncBufReadExt, AsyncWrite}; +use futures::{AsyncBufRead, AsyncBufReadExt as _, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; use parking_lot::Mutex; use serde_json::Value; @@ -45,46 +45,38 @@ pub struct DebugAdapterClient { sequence_count: AtomicU64, config: DebugAdapterConfig, io_handlers: Arc>>, - log_io_handlers: Option>>>, + io_log_handlers: Arc>>, } pub struct TransportParams { - rx: Box, - tx: Box, - err: Option>, - process: Option, + input: Box, + output: Box, + stdout: Option>, + stderr: Box, + process: Child, } impl TransportParams { + /// Defines communication channels for the debug adapter and logging: + /// - `input` and `output`: Communication with the debug adapter + /// - `stdin`, `stdout`, and `stderr`: Capture and log adapter process I/O pub fn new( - rx: Box, - tx: Box, - err: Option>, - process: Option, + input: Box, + output: Box, + stdout: Option>, + stderr: Box, + process: Child, ) -> Self { TransportParams { - rx, - tx, - err, + input, + output, + stdout, + stderr, process, } } } -pub struct AdapterLogIo { - out: Box, - err: Box, -} - -impl AdapterLogIo { - pub fn new( - out: Box, - err: Box, - ) -> Self { - Self { out, err } - } -} - impl DebugAdapterClient { pub async fn new( id: DebugAdapterClientId, @@ -92,7 +84,6 @@ impl DebugAdapterClient { request_args: Value, config: DebugAdapterConfig, transport_params: TransportParams, - adapter_log_io: Option, event_handler: F, cx: &mut AsyncAppContext, ) -> Result> @@ -100,49 +91,47 @@ impl DebugAdapterClient { F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { let io_handlers = Arc::new(Mutex::new(Vec::new())); - let log_io_handlers = match adapter_log_io { - Some(AdapterLogIo { out, err }) => { - let log_io_handlers = Arc::new(Mutex::new(Vec::new())); + let io_log_handlers = Arc::new(Mutex::new(Vec::new())); - Self::handle_adapter_logs(out, err, log_io_handlers.clone(), cx); - Some(log_io_handlers) - } - None => None, - }; + if let Some(stdout) = transport_params.stdout { + Self::handle_adapter_logs(stdout, io_log_handlers.clone(), cx); + } let transport = Self::handle_transport( - transport_params.rx, - transport_params.tx, - transport_params.err, + transport_params.input, + transport_params.output, + transport_params.stderr, event_handler, io_handlers.clone(), + io_log_handlers.clone(), cx, ); Ok(Arc::new(Self { id, - adapter_id, - request_args, config, transport, + adapter_id, io_handlers, - log_io_handlers, + request_args, + io_log_handlers, sequence_count: AtomicU64::new(1), - _process: Arc::new(Mutex::new(transport_params.process)), + _process: Arc::new(Mutex::new(Some(transport_params.process))), })) } pub fn handle_transport( - rx: Box, - tx: Box, - err: Option>, + stdin: Box, + stdout: Box, + stderr: Box, event_handler: F, io_handlers: Arc>>, + io_log_handlers: Arc>>, cx: &mut AsyncAppContext, ) -> Arc where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - let transport = Transport::start(rx, tx, err, io_handlers, cx); + let transport = Transport::start(stdin, stdout, stderr, io_handlers, io_log_handlers, cx); let server_rx = transport.server_rx.clone(); let server_tr = transport.server_tx.clone(); @@ -155,32 +144,21 @@ impl DebugAdapterClient { } fn handle_adapter_logs( - out: Box, - err: Box, - log_io_handlers: Arc>>, + stdout: Box, + io_log_handlers: Arc>>, cx: &mut AsyncAppContext, ) { - { - let mut out = out.lines(); - let log_io_handlers = log_io_handlers.clone(); - cx.spawn(|_| async move { + cx.spawn({ + let io_log_handlers = io_log_handlers.clone(); + |_| async move { use futures::stream::StreamExt; - while let Some(Ok(line)) = out.next().await { - for handler in log_io_handlers.lock().iter_mut() { + + let mut lines = stdout.lines(); + while let Some(Ok(line)) = lines.next().await { + for handler in io_log_handlers.lock().iter_mut() { handler(IoKind::StdOut, line.as_ref()); } } - }) - .detach(); - } - - let mut err = err.lines(); - cx.spawn(|_| async move { - use futures::stream::StreamExt; - while let Some(Ok(line)) = err.next().await { - for handler in log_io_handlers.lock().iter_mut() { - handler(IoKind::StdErr, line.as_ref()); - } } }) .detach(); @@ -250,7 +228,7 @@ impl DebugAdapterClient { } pub fn has_adapter_logs(&self) -> bool { - self.log_io_handlers.is_some() + true // TODO debugger: fix this when we move code to the transport layer } pub fn id(&self) -> DebugAdapterClientId { @@ -316,9 +294,7 @@ impl DebugAdapterClient { where F: 'static + Send + FnMut(IoKind, &str), { - if let Some(ref io_log_handlers) = self.log_io_handlers { - let mut io_log_handlers = io_log_handlers.lock(); - io_log_handlers.push(Box::new(f)); - } + let mut io_log_handlers = self.io_log_handlers.lock(); + io_log_handlers.push(Box::new(f)); } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index bcef01d9f62f03..f1fabdc2b0d063 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -31,10 +31,11 @@ pub struct Transport { impl Transport { pub fn start( - server_stdout: Box, server_stdin: Box, - server_stderr: Option>, + server_stdout: Box, + server_stderr: Box, io_handlers: Arc>>, + io_log_handlers: Arc>>, cx: &mut AsyncAppContext, ) -> Arc { let (client_tx, server_rx) = unbounded::(); @@ -52,11 +53,9 @@ impl Transport { )) .detach(); - if let Some(stderr) = server_stderr { - cx.background_executor() - .spawn(Self::err(stderr, io_handlers.clone())) - .detach(); - } + cx.background_executor() + .spawn(Self::err(server_stderr, io_log_handlers.clone())) + .detach(); cx.background_executor() .spawn(Self::send( diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index b2accf045be59c..a33e1802839b5b 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -28,15 +28,11 @@ impl DebugAdapter for CustomDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result<(TransportParams, Option)> { + ) -> Result { match &self.custom_args.connection { - DebugConnectionType::STDIO => { - create_stdio_client(adapter_binary).map(|transport| (transport, None)) - } + DebugConnectionType::STDIO => create_stdio_client(adapter_binary), DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), adapter_binary, cx) - .await - .map(|(transport, log_io)| (transport, Some(log_io))) + create_tcp_client(tcp_host.clone(), adapter_binary, cx).await } } } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 9f405ee989692d..fac4f3c77c7377 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -17,7 +17,7 @@ use dap::{ create_stdio_client, create_tcp_client, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, }, - client::{AdapterLogIo, TransportParams}, + client::TransportParams, }; use gpui::AsyncAppContext; use http_client::github::latest_github_release; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 6d23722e65c8f0..185a785a6ff4cb 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -22,16 +22,14 @@ impl DebugAdapter for JsDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result<(TransportParams, Option)> { + ) -> Result { let host = TCPHost { port: Some(8133), host: None, delay: Some(1000), }; - create_tcp_client(host, adapter_binary, cx) - .await - .map(|(transport, log_io)| (transport, Some(log_io))) + create_tcp_client(host, adapter_binary, cx).await } async fn fetch_binary( diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 0f8630a73f54c7..56cda6750a2752 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -25,8 +25,8 @@ impl DebugAdapter for LldbDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, _: &mut AsyncAppContext, - ) -> Result<(TransportParams, Option)> { - create_stdio_client(adapter_binary).map(|transport| (transport, None)) + ) -> Result { + create_stdio_client(adapter_binary) } async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 4d1be30132b294..62ecb16506bb82 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -22,16 +22,14 @@ impl DebugAdapter for PhpDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result<(TransportParams, Option)> { + ) -> Result { let host = TCPHost { port: Some(8132), host: None, delay: Some(1000), }; - create_tcp_client(host, adapter_binary, cx) - .await - .map(|(transport, log_io)| (transport, Some(log_io))) + create_tcp_client(host, adapter_binary, cx).await } async fn fetch_binary( diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 73fdec036f5604..12b16280f0de08 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -22,8 +22,8 @@ impl DebugAdapter for PythonDebugAdapter { &self, adapter_binary: &DebugAdapterBinary, _: &mut AsyncAppContext, - ) -> Result<(TransportParams, Option)> { - create_stdio_client(adapter_binary).map(|transport| (transport, None)) + ) -> Result { + create_stdio_client(adapter_binary) } async fn fetch_binary( diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index f3784ea3bfcb4e..bdcfc6bc3d23e5 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/crates/debugger_tools/Cargo.toml @@ -18,7 +18,6 @@ workspace.workspace = true editor.workspace = true project.workspace = true dap.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types" } serde_json.workspace = true futures.workspace = true anyhow.workspace = true diff --git a/crates/debugger_tools/TODO.md b/crates/debugger_tools/TODO.md deleted file mode 100644 index df0cae0b4b6168..00000000000000 --- a/crates/debugger_tools/TODO.md +++ /dev/null @@ -1 +0,0 @@ -- [ ] housekeeping: ensure references to debug adapters are cleaned when they are closed diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 97f7c7a48848d2..76da70e0eb6d33 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -21,7 +21,7 @@ use workspace::{ }; struct DapLogView { - pub(crate) editor: View, + editor: View, focus_handle: FocusHandle, log_store: Model, editor_subscriptions: Vec, @@ -52,9 +52,11 @@ struct RpcMessages { } impl RpcMessages { + const MESSAGE_QUEUE_LIMIT: usize = 255; + fn new() -> Self { Self { - messages: VecDeque::with_capacity(255), + messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), last_message_kind: None, } } @@ -158,11 +160,11 @@ impl LogStore { this.projects.remove(&weak_project); }), cx.subscribe(project, |this, project, event, cx| match event { - project::Event::DebugClientStarted(id) => { - let read_project = project.read(cx); - if let Some(client) = read_project.debug_client_for_id(*id, cx) { - this.add_debug_client(client.id(), Some(client), cx); - } + 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); @@ -246,7 +248,7 @@ impl LogStore { kind: LogKind, cx: &mut ModelContext, ) { - while log_lines.len() >= 255 { + while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { log_lines.pop_front(); } let entry: &str = message.as_ref(); @@ -261,12 +263,11 @@ impl LogStore { &mut self, client_id: DebugAdapterClientId, client: Option>, - cx: &mut ModelContext, ) -> Option<&mut DebugAdapterState> { - let client_state = self.debug_clients.entry(client_id).or_insert_with(|| { - cx.notify(); - DebugAdapterState::new() - }); + let client_state = self + .debug_clients + .entry(client_id) + .or_insert_with(DebugAdapterState::new); if let Some(client) = client { let io_tx = self.io_tx.clone(); @@ -276,14 +277,12 @@ impl LogStore { .ok(); }); - if client.has_adapter_logs() { - let log_io_tx = self.log_io_tx.clone(); - client.on_log_io(move |io_kind, message| { - log_io_tx - .unbounded_send((client_id, io_kind, message.to_string())) - .ok(); - }); - } + let log_io_tx = self.log_io_tx.clone(); + client.on_log_io(move |io_kind, message| { + log_io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }); } Some(client_state) @@ -308,7 +307,7 @@ impl LogStore { .debug_clients .get_mut(&client_id)? .rpc_messages - .get_or_insert_with(|| RpcMessages::new()); + .get_or_insert_with(RpcMessages::new); Some(&mut rpc_state.messages) } @@ -326,10 +325,6 @@ impl LogStore { self.debug_clients.get_mut(&client_id)?.rpc_messages.take(); Some(()) } - - // fn clients(&self) -> impl Iterator { - // self.debug_clients.iter() - // } } pub struct DapLogToolbarItemView { @@ -406,17 +401,15 @@ impl Render for DapLogToolbarItemView { let log_toolbar_view = log_toolbar_view.clone(); ContextMenu::build(cx, move |mut menu, cx| { for (ix, row) in menu_rows.into_iter().enumerate() { - menu = menu.header(format!("{}", row.client_name,)); - - if row.has_adapter_logs { - menu = menu.entry( - CLIENT_LOGS, - None, - cx.handler_for(&log_view, move |view, cx| { - view.show_log_messages_for_server(row.client_id, cx); - }), - ); - } + menu = menu.header(row.client_name.to_string()); + + menu = menu.entry( + CLIENT_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_log_messages_for_server(row.client_id, cx); + }), + ); menu = menu.custom_entry( { @@ -495,7 +488,7 @@ impl ToolbarItemView for DapLogToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn workspace::item::ItemHandle>, - _cx: &mut ViewContext, // TODO + cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { @@ -504,6 +497,9 @@ impl ToolbarItemView for DapLogToolbarItemView { } } self.log_view = None; + + cx.notify(); + workspace::ToolbarItemLocation::Hidden } } @@ -575,21 +571,19 @@ impl DapLogView { (editor, vec![editor_subscription, search_subscription]) } - fn menu_items<'a>(&self, cx: &'a AppContext) -> Option> { + fn menu_items(&self, cx: &AppContext) -> Option> { let log_store = self.log_store.read(cx); let mut rows = self .project .read(cx) .debug_clients(cx) - .filter_map(|client| { - Some(DapMenuItem { - client_id: client.id(), - client_name: client.config().kind.to_string(), - selected_entry: self.current_view.map_or(LogKind::Logs, |(_, kind)| kind), - has_adapter_logs: client.has_adapter_logs(), - rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), - }) + .map(|client| DapMenuItem { + client_id: client.id(), + client_name: client.config().kind.to_string(), + selected_entry: self.current_view.map_or(LogKind::Logs, |(_, kind)| kind), + has_adapter_logs: client.has_adapter_logs(), + rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), }) .collect::>(); rows.sort_by_key(|row| row.client_id); @@ -680,7 +674,6 @@ impl DapLogView { } }); if !enabled && Some(client_id) == self.current_view.map(|(client_id, _)| client_id) { - // self.show(client_id, cx); cx.notify(); } } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index a22f05734a129a..12715a96644cec 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -273,8 +273,7 @@ impl DapStore { merge_json_value_into(args.configuration, &mut request_args); } - let (transport_params, adapter_log_io) = - adapter.connect(&binary, &mut cx).await.log_err()?; + let transport_params = adapter.connect(&binary, &mut cx).await.log_err()?; let client = DebugAdapterClient::new( client_id, @@ -282,7 +281,6 @@ impl DapStore { request_args, config, transport_params, - adapter_log_io, move |message, cx| { dap_store .update(cx, |_, cx| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ea9afef283dd11..23a9e2e5495c1d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4249,8 +4249,6 @@ impl Project { Ok(()) } - - pub fn language_servers<'a>( &'a self, cx: &'a AppContext, @@ -4290,13 +4288,12 @@ impl Project { self.dap_store.read(cx).running_clients() } - pub fn debug_client_for_id( &self, - id: DebugAdapterClientId, + id: &DebugAdapterClientId, cx: &AppContext, ) -> Option> { - self.dap_store.read(cx).client_by_id(&id) + self.dap_store.read(cx).client_by_id(id) } } From 038718231418a7006196fcd07b4cf38e8c55c484 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 14:56:01 +0200 Subject: [PATCH 4/8] Implement `has_adapter_logs` for each transport impl --- crates/dap/src/client.rs | 8 ++++---- crates/dap/src/transport.rs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 6d3022da74ac94..672fa17c817c6c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -136,10 +136,6 @@ impl DebugAdapterClient { self.transport_delegate.send_message(message).await } - pub fn has_adapter_logs(&self) -> bool { - true // TODO debugger: fix this when we move code to the transport layer - } - pub fn id(&self) -> DebugAdapterClientId { self.id } @@ -169,6 +165,10 @@ impl DebugAdapterClient { 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), diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 3f64720a4c93af..48deaa50e3264e 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -320,6 +320,10 @@ 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), @@ -336,6 +340,8 @@ pub trait Transport: 'static + Send + Sync { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result; + + fn has_adapter_logs(&self) -> bool; } pub struct TcpTransport { @@ -433,6 +439,10 @@ impl Transport for TcpTransport { process, )) } + + fn has_adapter_logs(&self) -> bool { + true + } } pub struct StdioTransport {} @@ -492,4 +502,8 @@ impl Transport for StdioTransport { process, )) } + + fn has_adapter_logs(&self) -> bool { + false + } } From 984ec4d26d4502e06a80a87ec1e34c21d0ef0744 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 17:51:25 +0200 Subject: [PATCH 5/8] Make adapter stdout logging work --- crates/dap/src/transport.rs | 64 +++++++++++++----------- crates/debugger_tools/src/dap_log.rs | 75 ++++++++++++---------------- 2 files changed, 67 insertions(+), 72 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 48deaa50e3264e..993475fdb68f93 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -12,7 +12,8 @@ use smol::{ io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, net::{TcpListener, TcpStream}, - process::{self, Child}, + process::{self, Child, ChildStderr, ChildStdout}, + stream::StreamExt as _, }; use std::{ borrow::BorrowMut, @@ -28,9 +29,9 @@ use crate::adapters::DebugAdapterBinary; pub type IoHandler = Box; -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum LogKind { - Command, + Adapter, Rpc, } @@ -43,7 +44,6 @@ pub enum IoKind { pub struct TransportParams { input: Box, output: Box, - error: Box, process: Child, } @@ -51,13 +51,11 @@ impl TransportParams { pub fn new( input: Box, output: Box, - error: Box, process: Child, ) -> Self { TransportParams { input, output, - error, process, } } @@ -92,13 +90,16 @@ 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( @@ -109,9 +110,11 @@ impl TransportDelegate { )) .detach(); - cx.background_executor() - .spawn(Self::handle_error(params.error, self.log_handlers.clone())) - .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( @@ -123,6 +126,9 @@ impl TransportDelegate { )) .detach(); + self.process = Arc::new(Mutex::new(Some(params.process))); + self.server_tx = Some(server_tx.clone()); + Ok((server_rx, server_tx)) } @@ -146,6 +152,17 @@ impl TransportDelegate { } } + async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) { + let mut lines = BufReader::new(stdout).lines(); + while let Some(Ok(line)) = lines.next().await { + for (kind, handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Adapter) { + handler(IoKind::StdOut, line.as_ref()); + } + } + } + } + async fn handle_input( mut server_stdin: Box, client_rx: Receiver, @@ -211,14 +228,14 @@ impl TransportDelegate { Ok(()) } - async fn handle_error( - mut stderr: Box, - log_handlers: LogHandlers, - ) -> 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")); } @@ -399,15 +416,10 @@ impl Transport for TcpTransport { .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"))?, @@ -435,7 +447,6 @@ impl Transport for TcpTransport { Ok(TransportParams::new( Box::new(tx), Box::new(BufReader::new(rx)), - Box::new(BufReader::new(stderr)), process, )) } @@ -488,17 +499,12 @@ 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, )) } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index e0e3b013352486..e0cebe3c447e42 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,9 +1,12 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, - transport::IoKind, + transport::{IoKind, LogKind}, }; use editor::{Editor, EditorEvent}; -use futures::StreamExt; +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, @@ -35,8 +38,8 @@ struct DapLogView { struct LogStore { projects: HashMap, ProjectState>, debug_clients: HashMap, - io_tx: futures::channel::mpsc::UnboundedSender<(DebugAdapterClientId, IoKind, String)>, - log_io_tx: futures::channel::mpsc::UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + rpc_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + adapter_log_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, } struct ProjectState { @@ -93,13 +96,12 @@ impl DebugAdapterState { impl LogStore { fn new(cx: &ModelContext) -> Self { - let (io_tx, mut io_rx) = - futures::channel::mpsc::unbounded::<(DebugAdapterClientId, IoKind, String)>(); + 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)) = io_rx.next().await { + 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_io(server_id, io_kind, &message, cx); + this.on_rpc_log(server_id, io_kind, &message, cx); })?; } } @@ -107,13 +109,13 @@ impl LogStore { }) .detach_and_log_err(cx); - let (log_io_tx, mut log_io_rx) = - futures::channel::mpsc::unbounded::<(DebugAdapterClientId, IoKind, String)>(); + 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)) = log_io_rx.next().await { + 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_log_io(server_id, io_kind, &message, cx); + this.on_adapter_log(server_id, io_kind, &message, cx); })?; } } @@ -121,14 +123,14 @@ impl LogStore { }) .detach_and_log_err(cx); Self { + rpc_tx, + adapter_log_tx, projects: HashMap::new(), debug_clients: HashMap::new(), - io_tx, - log_io_tx, } } - fn on_io( + fn on_rpc_log( &mut self, client_id: DebugAdapterClientId, io_kind: IoKind, @@ -140,7 +142,7 @@ impl LogStore { Some(()) } - fn on_log_io( + fn on_adapter_log( &mut self, client_id: DebugAdapterClientId, io_kind: IoKind, @@ -238,7 +240,7 @@ impl LogStore { _ => message, }; - Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Logs, cx); + Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Adapter, cx); Some(()) } @@ -272,7 +274,7 @@ impl LogStore { .or_insert_with(DebugAdapterState::new); if let Some(client) = client { - let io_tx = self.io_tx.clone(); + let io_tx = self.rpc_tx.clone(); client.add_log_handler( move |io_kind, message| { @@ -280,17 +282,17 @@ impl LogStore { .unbounded_send((client_id, io_kind, message.to_string())) .ok(); }, - dap::transport::LogKind::Rpc, + LogKind::Rpc, ); - let log_io_tx = self.log_io_tx.clone(); + 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(); }, - dap::transport::LogKind::Command, + LogKind::Adapter, ); } @@ -396,7 +398,10 @@ impl Render for DapLogToolbarItemView { Cow::Owned(format!( "{} - {}", row.client_name, - row.selected_entry.label() + match row.selected_entry { + LogKind::Adapter => ADAPTER_LOGS, + LogKind::Rpc => RPC_MESSAGES, + } )) }) .unwrap_or_else(|| "No server selected".into()), @@ -413,7 +418,7 @@ impl Render for DapLogToolbarItemView { menu = menu.header(row.client_name.to_string()); menu = menu.entry( - CLIENT_LOGS, + ADAPTER_LOGS, None, cx.handler_for(&log_view, move |view, cx| { view.show_log_messages_for_server(row.client_id, cx); @@ -590,7 +595,7 @@ impl DapLogView { .map(|client| DapMenuItem { client_id: client.id(), client_name: client.config().kind.to_string(), - selected_entry: self.current_view.map_or(LogKind::Logs, |(_, kind)| kind), + selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), has_adapter_logs: client.has_adapter_logs(), rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), }) @@ -652,7 +657,7 @@ impl DapLogView { .map(|state| log_contents(&state)) }); if let Some(message_log) = message_log { - self.current_view = Some((client_id, LogKind::Logs)); + self.current_view = Some((client_id, LogKind::Adapter)); let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, cx); editor .read(cx) @@ -699,7 +704,7 @@ fn log_contents(lines: &VecDeque) -> String { }) } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub(crate) struct DapMenuItem { pub client_id: DebugAdapterClientId, pub client_name: String, @@ -708,25 +713,9 @@ pub(crate) struct DapMenuItem { pub selected_entry: LogKind, } -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum LogKind { - Rpc, - #[default] - Logs, -} - -const CLIENT_LOGS: &str = "Client Logs"; +const ADAPTER_LOGS: &str = "Adapter Logs"; const RPC_MESSAGES: &str = "RPC Messages"; -impl LogKind { - fn label(&self) -> &'static str { - match self { - LogKind::Rpc => RPC_MESSAGES, - LogKind::Logs => CLIENT_LOGS, - } - } -} - impl Render for DapLogView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { self.editor From 1f703ac0d1cc0bd96be0e0d0e352d71710dea846 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 18:01:44 +0200 Subject: [PATCH 6/8] Add conditional render for adapter log back --- crates/debugger_tools/src/dap_log.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index e0cebe3c447e42..27c94b6d1dda91 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -407,7 +407,6 @@ impl Render for DapLogToolbarItemView { .unwrap_or_else(|| "No server selected".into()), )) .menu({ - let _log_view = log_view.clone(); let log_toolbar_view = log_toolbar_view.clone(); move |cx| { let log_view = log_view.clone(); @@ -417,13 +416,15 @@ impl Render for DapLogToolbarItemView { for (ix, row) in menu_rows.into_iter().enumerate() { menu = menu.header(row.client_name.to_string()); - menu = menu.entry( - ADAPTER_LOGS, - None, - cx.handler_for(&log_view, move |view, cx| { - view.show_log_messages_for_server(row.client_id, cx); - }), - ); + 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( { From f589f877f70a30248e2e26f3dc5e8a111ae8cf8d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 19:20:08 +0200 Subject: [PATCH 7/8] Oops forgot to pipe the output --- crates/dap/src/transport.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 993475fdb68f93..1e767e2882cdca 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -13,7 +13,6 @@ use smol::{ lock::Mutex, net::{TcpListener, TcpStream}, process::{self, Child, ChildStderr, ChildStdout}, - stream::StreamExt as _, }; use std::{ borrow::BorrowMut, @@ -152,15 +151,19 @@ impl TransportDelegate { } } - async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) { - let mut lines = BufReader::new(stdout).lines(); - while let Some(Ok(line)) = lines.next().await { + 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_ref()); + handler(IoKind::StdOut, line.as_str()); } } + line.truncate(0); } + + Ok(()) } async fn handle_input( @@ -412,7 +415,7 @@ impl Transport for TcpTransport { command .stdin(Stdio::null()) - .stdout(Stdio::null()) + .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); From 70a76d8157546830bad8e55d812acf9906d69b42 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 19:34:48 +0200 Subject: [PATCH 8/8] 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. --- crates/debugger_tools/src/dap_log.rs | 215 ++++++++------------------- 1 file changed, 62 insertions(+), 153 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 27c94b6d1dda91..6a61edf1ad2620 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -21,7 +21,7 @@ use std::{ use workspace::{ item::Item, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, - ui::{h_flex, Button, Checkbox, Clickable, ContextMenu, Label, PopoverMenu, Selection}, + ui::{h_flex, Button, Clickable, ContextMenu, Label, PopoverMenu}, ToolbarItemEvent, ToolbarItemView, Workspace, }; @@ -48,7 +48,7 @@ struct ProjectState { struct DebugAdapterState { log_messages: VecDeque, - rpc_messages: Option, + rpc_messages: RpcMessages, } struct RpcMessages { @@ -61,8 +61,8 @@ impl RpcMessages { fn new() -> Self { Self { - messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), last_message_kind: None, + messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), } } } @@ -89,7 +89,7 @@ impl DebugAdapterState { fn new() -> Self { Self { log_messages: VecDeque::new(), - rpc_messages: None, + rpc_messages: RpcMessages::new(), } } } @@ -194,8 +194,10 @@ impl LogStore { io_kind: IoKind, message: String, cx: &mut ModelContext, - ) -> Option<()> { - let debug_client_state = self.get_debug_adapter_state(id)?; + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; let kind = match io_kind { IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, @@ -203,21 +205,17 @@ impl LogStore { }; let rpc_messages = &mut debug_client_state.rpc_messages; - if let Some(rpc_messages) = 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); + 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); } - - Some(()) + Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); } fn add_debug_client_log( @@ -226,8 +224,10 @@ impl LogStore { io_kind: IoKind, message: String, cx: &mut ModelContext, - ) -> Option<()> { - let debug_client_state = self.get_debug_adapter_state(id)?; + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; let mut log_messages = &mut debug_client_state.log_messages; @@ -241,8 +241,6 @@ impl LogStore { }; Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Adapter, cx); - - Some(()) } fn add_debug_client_entry( @@ -304,24 +302,6 @@ impl LogStore { cx.notify(); } - fn rpc_logging_enabled_for_client(&self, client_id: DebugAdapterClientId) -> bool { - self.debug_clients - .get(&client_id) - .is_some_and(|client| client.rpc_messages.is_some()) - } - - fn enable_rpc_trace_for_debug_client( - &mut self, - client_id: DebugAdapterClientId, - ) -> Option<&mut VecDeque> { - let rpc_state = self - .debug_clients - .get_mut(&client_id)? - .rpc_messages - .get_or_insert_with(RpcMessages::new); - Some(&mut rpc_state.messages) - } - fn log_messages_for_client( &mut self, client_id: DebugAdapterClientId, @@ -329,12 +309,17 @@ impl LogStore { Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) } - pub fn disable_rpc_trace_for_debug_client( + fn rpc_messages_for_client( &mut self, client_id: DebugAdapterClientId, - ) -> Option<()> { - self.debug_clients.get_mut(&client_id)?.rpc_messages.take(); - Some(()) + ) -> Option<&mut VecDeque> { + Some( + &mut self + .debug_clients + .get_mut(&client_id)? + .rpc_messages + .messages, + ) } } @@ -346,25 +331,6 @@ impl DapLogToolbarItemView { pub fn new() -> Self { Self { log_view: None } } - - fn toggle_rpc_logging_for_client( - &mut self, - id: DebugAdapterClientId, - enabled: bool, - cx: &mut ViewContext, - ) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |log_view, cx| { - log_view.toggle_rpc_trace_for_server(id, enabled, cx); - if enabled { - log_view.show_rpc_trace_for_server(id, cx); - cx.notify(); - } - cx.focus(&log_view.focus_handle); - }); - } - cx.notify(); - } } impl Render for DapLogToolbarItemView { @@ -382,13 +348,12 @@ impl Render for DapLogToolbarItemView { 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].clone()) + Some(&menu_rows[ix]) } else { None } }); - let log_toolbar_view = cx.view().clone(); let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") .anchor(AnchorCorner::TopLeft) .trigger(Button::new( @@ -406,73 +371,39 @@ impl Render for DapLogToolbarItemView { }) .unwrap_or_else(|| "No server selected".into()), )) - .menu({ - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - let log_view = log_view.clone(); - let menu_rows = menu_rows.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - ContextMenu::build(cx, move |mut menu, cx| { - for (ix, row) in menu_rows.into_iter().enumerate() { - 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( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().child( - Checkbox::new( - ix, - if row.rpc_trace_enabled { - Selection::Selected - } else { - Selection::Unselected - }, - ) - .on_click(cx.listener_for( - &log_toolbar_view, - move |view, selection, cx| { - let enabled = matches!( - selection, - Selection::Selected - ); - view.toggle_rpc_logging_for_client( - row.client_id, - enabled, - cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, + .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_rpc_trace_for_server(row.client_id, cx); + view.show_log_messages_for_server(row.client_id, cx); }), ); } - menu - }) - .into() - } + + 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( @@ -587,8 +518,6 @@ impl DapLogView { } fn menu_items(&self, cx: &AppContext) -> Option> { - let log_store = self.log_store.read(cx); - let mut rows = self .project .read(cx) @@ -598,7 +527,6 @@ impl DapLogView { 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(), - rpc_trace_enabled: log_store.rpc_logging_enabled_for_client(client.id()), }) .collect::>(); rows.sort_by_key(|row| row.client_id); @@ -613,7 +541,7 @@ impl DapLogView { ) { let rpc_log = self.log_store.update(cx, |log_store, _| { log_store - .enable_rpc_trace_for_debug_client(client_id) + .rpc_messages_for_client(client_id) .map(|state| log_contents(&state)) }); if let Some(rpc_log) = rpc_log { @@ -674,24 +602,6 @@ impl DapLogView { cx.focus(&self.focus_handle); } - - fn toggle_rpc_trace_for_server( - &mut self, - client_id: DebugAdapterClientId, - enabled: bool, - cx: &mut ViewContext, - ) { - self.log_store.update(cx, |log_store, _| { - if enabled { - log_store.enable_rpc_trace_for_debug_client(client_id); - } else { - log_store.disable_rpc_trace_for_debug_client(client_id); - } - }); - if !enabled && Some(client_id) == self.current_view.map(|(client_id, _)| client_id) { - cx.notify(); - } - } } fn log_contents(lines: &VecDeque) -> String { @@ -710,7 +620,6 @@ pub(crate) struct DapMenuItem { pub client_id: DebugAdapterClientId, pub client_name: String, pub has_adapter_logs: bool, - pub rpc_trace_enabled: bool, pub selected_entry: LogKind, }