use dap::{ adapters::DebugAdapterName, client::SessionId, debugger_settings::DebuggerSettings, transport::{IoKind, LogKind},};use editor::{Editor, EditorEvent};use futures::{ StreamExt, channel::mpsc::{UnboundedSender, unbounded},};use gpui::{ App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div,};use project::{ Project, debugger::{dap_store, session::Session}, search::SearchQuery,};use settings::Settings as _;use std::{ borrow::Cow, collections::{HashMap, VecDeque}, sync::Arc,};use util::maybe;use workspace::{ ToolbarItemEvent, ToolbarItemView, Workspace, item::Item, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},}; // TODO:// - [x] stop sorting by session ID// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)// - [ ] dump the launch/attach request somewhere (logs?) const MAX_SESSIONS: usize = 10; struct DapLogView { editor: Entity<Editor>, focus_handle: FocusHandle, log_store: Entity<LogStore>, editor_subscriptions: Vec<Subscription>, current_view: Option<(SessionId, LogKind)>, project: Entity<Project>, _subscriptions: Vec<Subscription>,} pub struct LogStore { projects: HashMap<WeakEntity<Project>, ProjectState>, debug_sessions: VecDeque<DebugAdapterState>, rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,} struct ProjectState { _subscriptions: [gpui::Subscription; 2],} struct DebugAdapterState { id: SessionId, log_messages: VecDeque<SharedString>, rpc_messages: RpcMessages, adapter_name: DebugAdapterName, has_adapter_logs: bool, is_terminated: bool,} struct RpcMessages { messages: VecDeque<SharedString>, last_message_kind: Option<MessageKind>, initialization_sequence: Vec<SharedString>, last_init_message_kind: Option<MessageKind>,} impl RpcMessages { const MESSAGE_QUEUE_LIMIT: usize = 255; fn new() -> Self { Self { last_message_kind: None, last_init_message_kind: None, messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), initialization_sequence: Vec::new(), } }} 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(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self { Self { id, log_messages: VecDeque::new(), rpc_messages: RpcMessages::new(), adapter_name, has_adapter_logs, is_terminated: false, } }} impl LogStore { pub fn new(cx: &Context<Self>) -> Self { let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); cx.spawn(async move |this, cx| { while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.add_debug_adapter_message(session_id, io_kind, command, message, cx); })?; } smol::future::yield_now().await; } anyhow::Ok(()) }) .detach_and_log_err(cx); let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); cx.spawn(async move |this, cx| { while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.add_debug_adapter_log(session_id, io_kind, message, cx); })?; } smol::future::yield_now().await; } anyhow::Ok(()) }) .detach_and_log_err(cx); Self { rpc_tx, adapter_log_tx, projects: HashMap::new(), debug_sessions: Default::default(), } } pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { 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.read(cx).dap_store(), |this, dap_store, event, cx| match event { dap_store::DapStoreEvent::DebugClientStarted(session_id) => { let session = dap_store.read(cx).session_by_id(session_id); if let Some(session) = session { this.add_debug_session(*session_id, session, cx); } } dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { this.get_debug_adapter_state(*session_id) .iter_mut() .for_each(|state| state.is_terminated = true); this.clean_sessions(cx); } _ => {} }, ), ], }, ); } fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { self.debug_sessions .iter_mut() .find(|adapter_state| adapter_state.id == id) } fn add_debug_adapter_message( &mut self, id: SessionId, io_kind: IoKind, command: Option<SharedString>, message: SharedString, cx: &mut Context<Self>, ) { let Some(debug_client_state) = self.get_debug_adapter_state(id) else { return; }; let is_init_seq = command.as_ref().is_some_and(|command| { matches!( command.as_ref(), "attach" | "launch" | "initialize" | "configurationDone" ) }); let kind = match io_kind { IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, IoKind::StdIn => MessageKind::Send, }; let rpc_messages = &mut debug_client_state.rpc_messages; // Push a separator if the kind has changed if rpc_messages.last_message_kind != Some(kind) { Self::get_debug_adapter_entry( &mut rpc_messages.messages, id, kind.label().into(), LogKind::Rpc, cx, ); rpc_messages.last_message_kind = Some(kind); } let entry = Self::get_debug_adapter_entry( &mut rpc_messages.messages, id, message, LogKind::Rpc, cx, ); if is_init_seq { if rpc_messages.last_init_message_kind != Some(kind) { rpc_messages .initialization_sequence .push(SharedString::from(kind.label())); rpc_messages.last_init_message_kind = Some(kind); } rpc_messages.initialization_sequence.push(entry); } cx.notify(); } fn add_debug_adapter_log( &mut self, id: SessionId, io_kind: IoKind, message: SharedString, cx: &mut Context<Self>, ) { let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else { return; }; let message = match io_kind { IoKind::StdErr => format!("stderr: {message}").into(), _ => message, }; Self::get_debug_adapter_entry( &mut debug_adapter_state.log_messages, id, message, LogKind::Adapter, cx, ); cx.notify(); } fn get_debug_adapter_entry( log_lines: &mut VecDeque<SharedString>, id: SessionId, message: SharedString, kind: LogKind, cx: &mut Context<Self>, ) -> SharedString { while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { log_lines.pop_front(); } let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages; let entry = if format_messages { maybe!({ serde_json::to_string_pretty::<serde_json::Value>( &serde_json::from_str(&message).ok()?, ) .ok() }) .map(SharedString::from) .unwrap_or(message) } else { message }; log_lines.push_back(entry.clone()); cx.emit(Event::NewLogEntry { id, entry: entry.clone(), kind, }); entry } fn add_debug_session( &mut self, session_id: SessionId, session: Entity<Session>, cx: &mut Context<Self>, ) { if self .debug_sessions .iter_mut() .any(|adapter_state| adapter_state.id == session_id) { return; } let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| { ( session.adapter(), session .adapter_client() .map(|client| client.has_adapter_logs()) .unwrap_or(false), ) }); self.debug_sessions.push_back(DebugAdapterState::new( session_id, adapter_name, has_adapter_logs, )); self.clean_sessions(cx); let io_tx = self.rpc_tx.clone(); let Some(client) = session.read(cx).adapter_client() else { return; }; client.add_log_handler( move |io_kind, command, message| { io_tx .unbounded_send(( session_id, io_kind, command.map(|command| command.to_owned().into()), message.to_owned().into(), )) .ok(); }, LogKind::Rpc, ); let log_io_tx = self.adapter_log_tx.clone(); client.add_log_handler( move |io_kind, command, message| { log_io_tx .unbounded_send(( session_id, io_kind, command.map(|command| command.to_owned().into()), message.to_owned().into(), )) .ok(); }, LogKind::Adapter, ); } fn clean_sessions(&mut self, cx: &mut Context<Self>) { let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS); self.debug_sessions.retain(|session| { if to_remove > 0 && session.is_terminated { to_remove -= 1; return false; } true }); cx.notify(); } fn log_messages_for_session( &mut self, session_id: SessionId, ) -> Option<&mut VecDeque<SharedString>> { self.debug_sessions .iter_mut() .find(|session| session.id == session_id) .map(|state| &mut state.log_messages) } fn rpc_messages_for_session( &mut self, session_id: SessionId, ) -> Option<&mut VecDeque<SharedString>> { self.debug_sessions.iter_mut().find_map(|state| { if state.id == session_id { Some(&mut state.rpc_messages.messages) } else { None } }) } fn initialization_sequence_for_session( &mut self, session_id: SessionId, ) -> Option<&mut Vec<SharedString>> { self.debug_sessions.iter_mut().find_map(|state| { if state.id == session_id { Some(&mut state.rpc_messages.initialization_sequence) } else { None } }) }} pub struct DapLogToolbarItemView { log_view: Option<Entity<DapLogView>>,} impl DapLogToolbarItemView { pub fn new() -> Self { Self { log_view: None } }} impl Render for DapLogToolbarItemView { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let Some(log_view) = self.log_view.clone() else { return Empty.into_any_element(); }; let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| { ( log_view.menu_items(cx), log_view.current_view.map(|(session_id, _)| session_id), ) }); let current_client = current_session_id .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id)); let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") .anchor(gpui::Corner::TopLeft) .trigger(Button::new( "debug_client_menu_header", current_client .map(|sub_item| { Cow::Owned(format!( "{} ({}) - {}", sub_item.adapter_name, sub_item.session_id.0, match sub_item.selected_entry { LogKind::Adapter => ADAPTER_LOGS, LogKind::Rpc => RPC_MESSAGES, } )) }) .unwrap_or_else(|| "No adapter selected".into()), )) .menu(move |mut window, cx| { let log_view = log_view.clone(); let menu_rows = menu_rows.clone(); ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { menu = menu.custom_row(move |_window, _cx| { div() .w_full() .pl_2() .child( Label::new(format!( "{}. {}", row.session_id.0, row.adapter_name, )) .color(workspace::ui::Color::Muted), ) .into_any_element()Lines 1–500 of 1038. Load more
use dap::{ adapters::DebugAdapterName, client::SessionId, debugger_settings::DebuggerSettings, transport::{IoKind, LogKind}, }; use editor::{Editor, EditorEvent}; use futures::{ StreamExt, channel::mpsc::{UnboundedSender, unbounded}, }; use gpui::{ App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, }; use project::{ Project, debugger::{dap_store, session::Session}, search::SearchQuery, }; use settings::Settings as _; use std::{ borrow::Cow, collections::{HashMap, VecDeque}, sync::Arc, }; use util::maybe; use workspace::{ ToolbarItemEvent, ToolbarItemView, Workspace, item::Item, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, }; // TODO: // - [x] stop sorting by session ID // - [x] pick the most recent session by default (logs if available, RPC messages otherwise) // - [ ] dump the launch/attach request somewhere (logs?) const MAX_SESSIONS: usize = 10; struct DapLogView { editor: Entity<Editor>, focus_handle: FocusHandle, log_store: Entity<LogStore>, editor_subscriptions: Vec<Subscription>, current_view: Option<(SessionId, LogKind)>, project: Entity<Project>, _subscriptions: Vec<Subscription>, } pub struct LogStore { projects: HashMap<WeakEntity<Project>, ProjectState>, debug_sessions: VecDeque<DebugAdapterState>, rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, } struct ProjectState { _subscriptions: [gpui::Subscription; 2], } struct DebugAdapterState { id: SessionId, log_messages: VecDeque<SharedString>, rpc_messages: RpcMessages, adapter_name: DebugAdapterName, has_adapter_logs: bool, is_terminated: bool, } struct RpcMessages { messages: VecDeque<SharedString>, last_message_kind: Option<MessageKind>, initialization_sequence: Vec<SharedString>, last_init_message_kind: Option<MessageKind>, } impl RpcMessages { const MESSAGE_QUEUE_LIMIT: usize = 255; fn new() -> Self { Self { last_message_kind: None, last_init_message_kind: None, messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), initialization_sequence: Vec::new(), } } } 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(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self { Self { id, log_messages: VecDeque::new(), rpc_messages: RpcMessages::new(), adapter_name, has_adapter_logs, is_terminated: false, } } } impl LogStore { pub fn new(cx: &Context<Self>) -> Self { let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); cx.spawn(async move |this, cx| { while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.add_debug_adapter_message(session_id, io_kind, command, message, cx); })?; } smol::future::yield_now().await; } anyhow::Ok(()) }) .detach_and_log_err(cx); let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>(); cx.spawn(async move |this, cx| { while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.add_debug_adapter_log(session_id, io_kind, message, cx); })?; } smol::future::yield_now().await; } anyhow::Ok(()) }) .detach_and_log_err(cx); Self { rpc_tx, adapter_log_tx, projects: HashMap::new(), debug_sessions: Default::default(), } } pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { 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.read(cx).dap_store(), |this, dap_store, event, cx| match event { dap_store::DapStoreEvent::DebugClientStarted(session_id) => { let session = dap_store.read(cx).session_by_id(session_id); if let Some(session) = session { this.add_debug_session(*session_id, session, cx); } } dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { this.get_debug_adapter_state(*session_id) .iter_mut() .for_each(|state| state.is_terminated = true); this.clean_sessions(cx); } _ => {} }, ), ], }, ); } fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { self.debug_sessions .iter_mut() .find(|adapter_state| adapter_state.id == id) } fn add_debug_adapter_message( &mut self, id: SessionId, io_kind: IoKind, command: Option<SharedString>, message: SharedString, cx: &mut Context<Self>, ) { let Some(debug_client_state) = self.get_debug_adapter_state(id) else { return; }; let is_init_seq = command.as_ref().is_some_and(|command| { matches!( command.as_ref(), "attach" | "launch" | "initialize" | "configurationDone" ) }); let kind = match io_kind { IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, IoKind::StdIn => MessageKind::Send, }; let rpc_messages = &mut debug_client_state.rpc_messages; // Push a separator if the kind has changed if rpc_messages.last_message_kind != Some(kind) { Self::get_debug_adapter_entry( &mut rpc_messages.messages, id, kind.label().into(), LogKind::Rpc, cx, ); rpc_messages.last_message_kind = Some(kind); } let entry = Self::get_debug_adapter_entry( &mut rpc_messages.messages, id, message, LogKind::Rpc, cx, ); if is_init_seq { if rpc_messages.last_init_message_kind != Some(kind) { rpc_messages .initialization_sequence .push(SharedString::from(kind.label())); rpc_messages.last_init_message_kind = Some(kind); } rpc_messages.initialization_sequence.push(entry); } cx.notify(); } fn add_debug_adapter_log( &mut self, id: SessionId, io_kind: IoKind, message: SharedString, cx: &mut Context<Self>, ) { let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else { return; }; let message = match io_kind { IoKind::StdErr => format!("stderr: {message}").into(), _ => message, }; Self::get_debug_adapter_entry( &mut debug_adapter_state.log_messages, id, message, LogKind::Adapter, cx, ); cx.notify(); } fn get_debug_adapter_entry( log_lines: &mut VecDeque<SharedString>, id: SessionId, message: SharedString, kind: LogKind, cx: &mut Context<Self>, ) -> SharedString { while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { log_lines.pop_front(); } let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages; let entry = if format_messages { maybe!({ serde_json::to_string_pretty::<serde_json::Value>( &serde_json::from_str(&message).ok()?, ) .ok() }) .map(SharedString::from) .unwrap_or(message) } else { message }; log_lines.push_back(entry.clone()); cx.emit(Event::NewLogEntry { id, entry: entry.clone(), kind, }); entry } fn add_debug_session( &mut self, session_id: SessionId, session: Entity<Session>, cx: &mut Context<Self>, ) { if self .debug_sessions .iter_mut() .any(|adapter_state| adapter_state.id == session_id) { return; } let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| { ( session.adapter(), session .adapter_client() .map(|client| client.has_adapter_logs()) .unwrap_or(false), ) }); self.debug_sessions.push_back(DebugAdapterState::new( session_id, adapter_name, has_adapter_logs, )); self.clean_sessions(cx); let io_tx = self.rpc_tx.clone(); let Some(client) = session.read(cx).adapter_client() else { return; }; client.add_log_handler( move |io_kind, command, message| { io_tx .unbounded_send(( session_id, io_kind, command.map(|command| command.to_owned().into()), message.to_owned().into(), )) .ok(); }, LogKind::Rpc, ); let log_io_tx = self.adapter_log_tx.clone(); client.add_log_handler( move |io_kind, command, message| { log_io_tx .unbounded_send(( session_id, io_kind, command.map(|command| command.to_owned().into()), message.to_owned().into(), )) .ok(); }, LogKind::Adapter, ); } fn clean_sessions(&mut self, cx: &mut Context<Self>) { let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS); self.debug_sessions.retain(|session| { if to_remove > 0 && session.is_terminated { to_remove -= 1; return false; } true }); cx.notify(); } fn log_messages_for_session( &mut self, session_id: SessionId, ) -> Option<&mut VecDeque<SharedString>> { self.debug_sessions .iter_mut() .find(|session| session.id == session_id) .map(|state| &mut state.log_messages) } fn rpc_messages_for_session( &mut self, session_id: SessionId, ) -> Option<&mut VecDeque<SharedString>> { self.debug_sessions.iter_mut().find_map(|state| { if state.id == session_id { Some(&mut state.rpc_messages.messages) } else { None } }) } fn initialization_sequence_for_session( &mut self, session_id: SessionId, ) -> Option<&mut Vec<SharedString>> { self.debug_sessions.iter_mut().find_map(|state| { if state.id == session_id { Some(&mut state.rpc_messages.initialization_sequence) } else { None } }) } } pub struct DapLogToolbarItemView { log_view: Option<Entity<DapLogView>>, } impl DapLogToolbarItemView { pub fn new() -> Self { Self { log_view: None } } } impl Render for DapLogToolbarItemView { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let Some(log_view) = self.log_view.clone() else { return Empty.into_any_element(); }; let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| { ( log_view.menu_items(cx), log_view.current_view.map(|(session_id, _)| session_id), ) }); let current_client = current_session_id .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id)); let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") .anchor(gpui::Corner::TopLeft) .trigger(Button::new( "debug_client_menu_header", current_client .map(|sub_item| { Cow::Owned(format!( "{} ({}) - {}", sub_item.adapter_name, sub_item.session_id.0, match sub_item.selected_entry { LogKind::Adapter => ADAPTER_LOGS, LogKind::Rpc => RPC_MESSAGES, } )) }) .unwrap_or_else(|| "No adapter selected".into()), )) .menu(move |mut window, cx| { let log_view = log_view.clone(); let menu_rows = menu_rows.clone(); ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { menu = menu.custom_row(move |_window, _cx| { div() .w_full() .pl_2() .child( Label::new(format!( "{}. {}", row.session_id.0, row.adapter_name, )) .color(workspace::ui::Color::Muted), ) .into_any_element()