debugger: Fix DAP Logs mangling sessions across multiple Zed windows (#33656)

Release Notes:

- Fixed an issue with Debug Adapter log showing sessions from other Zed
windows in the dropdown.
This commit is contained in:
Piotr Osiewicz 2025-06-30 17:01:54 +02:00 committed by GitHub
parent bdf29bf76f
commit e5a8cc7aab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 285 additions and 207 deletions

View file

@ -21,7 +21,7 @@ use project::{
use settings::Settings as _; use settings::Settings as _;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{HashMap, VecDeque}, collections::{BTreeMap, HashMap, VecDeque},
sync::Arc, sync::Arc,
}; };
use util::maybe; use util::maybe;
@ -32,13 +32,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, 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 { struct DapLogView {
editor: Entity<Editor>, editor: Entity<Editor>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -49,14 +42,34 @@ struct DapLogView {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
struct LogStoreEntryIdentifier<'a> {
session_id: SessionId,
project: Cow<'a, WeakEntity<Project>>,
}
impl LogStoreEntryIdentifier<'_> {
fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
LogStoreEntryIdentifier {
session_id: self.session_id,
project: Cow::Owned(self.project.as_ref().clone()),
}
}
}
struct LogStoreMessage {
id: LogStoreEntryIdentifier<'static>,
kind: IoKind,
command: Option<SharedString>,
message: SharedString,
}
pub struct LogStore { pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>, projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_sessions: VecDeque<DebugAdapterState>, rpc_tx: UnboundedSender<LogStoreMessage>,
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>, adapter_log_tx: UnboundedSender<LogStoreMessage>,
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
} }
struct ProjectState { struct ProjectState {
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
_subscriptions: [gpui::Subscription; 2], _subscriptions: [gpui::Subscription; 2],
} }
@ -122,13 +135,12 @@ impl DebugAdapterState {
impl LogStore { impl LogStore {
pub fn new(cx: &Context<Self>) -> Self { pub fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) = let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await { while let Some(message) = rpc_rx.next().await {
if let Some(this) = this.upgrade() { if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.add_debug_adapter_message(session_id, io_kind, command, message, cx); this.add_debug_adapter_message(message, cx);
})?; })?;
} }
@ -138,13 +150,12 @@ impl LogStore {
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
let (adapter_log_tx, mut adapter_log_rx) = let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await { while let Some(message) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() { if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.add_debug_adapter_log(session_id, io_kind, message, cx); this.add_debug_adapter_log(message, cx);
})?; })?;
} }
@ -157,57 +168,76 @@ impl LogStore {
rpc_tx, rpc_tx,
adapter_log_tx, adapter_log_tx,
projects: HashMap::new(), projects: HashMap::new(),
debug_sessions: Default::default(),
} }
} }
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
let weak_project = project.downgrade();
self.projects.insert( self.projects.insert(
project.downgrade(), project.downgrade(),
ProjectState { ProjectState {
_subscriptions: [ _subscriptions: [
cx.observe_release(project, move |this, _, _| { cx.observe_release(project, {
let weak_project = project.downgrade();
move |this, _, _| {
this.projects.remove(&weak_project); this.projects.remove(&weak_project);
}
}), }),
cx.subscribe( cx.subscribe(&project.read(cx).dap_store(), {
&project.read(cx).dap_store(), let weak_project = project.downgrade();
|this, dap_store, event, cx| match event { move |this, dap_store, event, cx| match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => { dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id); let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session { if let Some(session) = session {
this.add_debug_session(*session_id, session, cx); this.add_debug_session(
LogStoreEntryIdentifier {
project: Cow::Owned(weak_project.clone()),
session_id: *session_id,
},
session,
cx,
);
} }
} }
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
this.get_debug_adapter_state(*session_id) let id = LogStoreEntryIdentifier {
.iter_mut() project: Cow::Borrowed(&weak_project),
.for_each(|state| state.is_terminated = true); session_id: *session_id,
};
if let Some(state) = this.get_debug_adapter_state(&id) {
state.is_terminated = true;
}
this.clean_sessions(cx); this.clean_sessions(cx);
} }
_ => {} _ => {}
}, }
), }),
], ],
debug_sessions: Default::default(),
}, },
); );
} }
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { fn get_debug_adapter_state(
self.debug_sessions &mut self,
.iter_mut() id: &LogStoreEntryIdentifier<'_>,
.find(|adapter_state| adapter_state.id == id) ) -> Option<&mut DebugAdapterState> {
self.projects
.get_mut(&id.project)
.and_then(|state| state.debug_sessions.get_mut(&id.session_id))
} }
fn add_debug_adapter_message( fn add_debug_adapter_message(
&mut self, &mut self,
id: SessionId, LogStoreMessage {
io_kind: IoKind, id,
command: Option<SharedString>, kind: io_kind,
message: SharedString, command,
message,
}: LogStoreMessage,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(debug_client_state) = self.get_debug_adapter_state(id) else { let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
return; return;
}; };
@ -229,7 +259,7 @@ impl LogStore {
if rpc_messages.last_message_kind != Some(kind) { if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry( Self::get_debug_adapter_entry(
&mut rpc_messages.messages, &mut rpc_messages.messages,
id, id.to_owned(),
kind.label().into(), kind.label().into(),
LogKind::Rpc, LogKind::Rpc,
cx, cx,
@ -239,7 +269,7 @@ impl LogStore {
let entry = Self::get_debug_adapter_entry( let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages, &mut rpc_messages.messages,
id, id.to_owned(),
message, message,
LogKind::Rpc, LogKind::Rpc,
cx, cx,
@ -260,12 +290,15 @@ impl LogStore {
fn add_debug_adapter_log( fn add_debug_adapter_log(
&mut self, &mut self,
id: SessionId, LogStoreMessage {
io_kind: IoKind, id,
message: SharedString, kind: io_kind,
message,
..
}: LogStoreMessage,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else { let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
return; return;
}; };
@ -276,7 +309,7 @@ impl LogStore {
Self::get_debug_adapter_entry( Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages, &mut debug_adapter_state.log_messages,
id, id.to_owned(),
message, message,
LogKind::Adapter, LogKind::Adapter,
cx, cx,
@ -286,13 +319,17 @@ impl LogStore {
fn get_debug_adapter_entry( fn get_debug_adapter_entry(
log_lines: &mut VecDeque<SharedString>, log_lines: &mut VecDeque<SharedString>,
id: SessionId, id: LogStoreEntryIdentifier<'static>,
message: SharedString, message: SharedString,
kind: LogKind, kind: LogKind,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> SharedString { ) -> SharedString {
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { if let Some(excess) = log_lines
log_lines.pop_front(); .len()
.checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
&& excess > 0
{
log_lines.drain(..excess);
} }
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages; let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
@ -322,30 +359,29 @@ impl LogStore {
fn add_debug_session( fn add_debug_session(
&mut self, &mut self,
session_id: SessionId, id: LogStoreEntryIdentifier<'static>,
session: Entity<Session>, session: Entity<Session>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self maybe!({
.debug_sessions let project_entry = self.projects.get_mut(&id.project)?;
.iter_mut() let std::collections::btree_map::Entry::Vacant(state) =
.any(|adapter_state| adapter_state.id == session_id) project_entry.debug_sessions.entry(id.session_id)
{ else {
return; return None;
} };
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| { let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
( (
session.adapter(), session.adapter(),
session session
.adapter_client() .adapter_client()
.map(|client| client.has_adapter_logs()) .map_or(false, |client| client.has_adapter_logs()),
.unwrap_or(false),
) )
}); });
self.debug_sessions.push_back(DebugAdapterState::new( state.insert(DebugAdapterState::new(
session_id, id.session_id,
adapter_name, adapter_name,
has_adapter_logs, has_adapter_logs,
)); ));
@ -354,86 +390,80 @@ impl LogStore {
let io_tx = self.rpc_tx.clone(); let io_tx = self.rpc_tx.clone();
let Some(client) = session.read(cx).adapter_client() else { let client = session.read(cx).adapter_client()?;
return; let project = id.project.clone();
}; let session_id = id.session_id;
client.add_log_handler( client.add_log_handler(
move |io_kind, command, message| { move |kind, command, message| {
io_tx io_tx
.unbounded_send(( .unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id, session_id,
io_kind, project: project.clone(),
command.map(|command| command.to_owned().into()), },
message.to_owned().into(), kind,
)) command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok(); .ok();
}, },
LogKind::Rpc, LogKind::Rpc,
); );
let log_io_tx = self.adapter_log_tx.clone(); let log_io_tx = self.adapter_log_tx.clone();
let project = id.project;
client.add_log_handler( client.add_log_handler(
move |io_kind, command, message| { move |kind, command, message| {
log_io_tx log_io_tx
.unbounded_send(( .unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id, session_id,
io_kind, project: project.clone(),
command.map(|command| command.to_owned().into()), },
message.to_owned().into(), kind,
)) command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok(); .ok();
}, },
LogKind::Adapter, LogKind::Adapter,
); );
Some(())
});
} }
fn clean_sessions(&mut self, cx: &mut Context<Self>) { fn clean_sessions(&mut self, cx: &mut Context<Self>) {
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS); self.projects.values_mut().for_each(|project| {
self.debug_sessions.retain(|session| { project
if to_remove > 0 && session.is_terminated { .debug_sessions
to_remove -= 1; .retain(|_, session| !session.is_terminated);
return false;
}
true
}); });
cx.notify(); cx.notify();
} }
fn log_messages_for_session( fn log_messages_for_session(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> { ) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions self.get_debug_adapter_state(id)
.iter_mut()
.find(|session| session.id == session_id)
.map(|state| &mut state.log_messages) .map(|state| &mut state.log_messages)
} }
fn rpc_messages_for_session( fn rpc_messages_for_session(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> { ) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| { self.get_debug_adapter_state(id)
if state.id == session_id { .map(|state| &mut state.rpc_messages.messages)
Some(&mut state.rpc_messages.messages)
} else {
None
}
})
} }
fn initialization_sequence_for_session( fn initialization_sequence_for_session(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut Vec<SharedString>> { ) -> Option<&Vec<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| { self.get_debug_adapter_state(&id)
if state.id == session_id { .map(|state| &state.rpc_messages.initialization_sequence)
Some(&mut state.rpc_messages.initialization_sequence)
} else {
None
}
})
} }
} }
@ -453,10 +483,11 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element(); return Empty.into_any_element();
}; };
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| { let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
( (
log_view.menu_items(cx), log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id), log_view.current_view.map(|(session_id, _)| session_id),
log_view.project.downgrade(),
) )
}); });
@ -484,6 +515,7 @@ impl Render for DapLogToolbarItemView {
.menu(move |mut window, cx| { .menu(move |mut window, cx| {
let log_view = log_view.clone(); let log_view = log_view.clone();
let menu_rows = menu_rows.clone(); let menu_rows = menu_rows.clone();
let project = project.clone();
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() { for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| { menu = menu.custom_row(move |_window, _cx| {
@ -509,8 +541,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(ADAPTER_LOGS)) .child(Label::new(ADAPTER_LOGS))
.into_any_element() .into_any_element()
}, },
window.handler_for(&log_view, move |view, window, cx| { window.handler_for(&log_view, {
view.show_log_messages_for_adapter(row.session_id, window, cx); let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_log_messages_for_adapter(&id, window, cx);
}
}), }),
); );
} }
@ -524,8 +563,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(RPC_MESSAGES)) .child(Label::new(RPC_MESSAGES))
.into_any_element() .into_any_element()
}, },
window.handler_for(&log_view, move |view, window, cx| { window.handler_for(&log_view, {
view.show_rpc_trace_for_server(row.session_id, window, cx); let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_rpc_trace_for_server(&id, window, cx);
}
}), }),
) )
.custom_entry( .custom_entry(
@ -536,12 +582,17 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(INITIALIZATION_SEQUENCE)) .child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element() .into_any_element()
}, },
window.handler_for(&log_view, move |view, window, cx| { window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_initialization_sequence_for_server( view.show_initialization_sequence_for_server(
row.session_id, &id, window, cx,
window,
cx,
); );
}
}), }),
); );
} }
@ -613,7 +664,9 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event { let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => { Event::NewLogEntry { id, entry, kind } => {
if log_view.current_view == Some((*id, *kind)) { if log_view.current_view == Some((id.session_id, *kind))
&& log_view.project == *id.project
{
log_view.editor.update(cx, |editor, cx| { log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false); editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx); let last_point = editor.buffer().read(cx).len(cx);
@ -629,12 +682,18 @@ impl DapLogView {
} }
} }
}); });
let weak_project = project.downgrade();
let state_info = log_store let state_info = log_store
.read(cx) .read(cx)
.projects
.get(&weak_project)
.and_then(|project| {
project
.debug_sessions .debug_sessions
.back() .values()
.map(|session| (session.id, session.has_adapter_logs)); .next_back()
.map(|session| (session.id, session.has_adapter_logs))
});
let mut this = Self { let mut this = Self {
editor, editor,
@ -647,10 +706,14 @@ impl DapLogView {
}; };
if let Some((session_id, have_adapter_logs)) = state_info { if let Some((session_id, have_adapter_logs)) = state_info {
let id = LogStoreEntryIdentifier {
session_id,
project: Cow::Owned(weak_project),
};
if have_adapter_logs { if have_adapter_logs {
this.show_log_messages_for_adapter(session_id, window, cx); this.show_log_messages_for_adapter(&id, window, cx);
} else { } else {
this.show_rpc_trace_for_server(session_id, window, cx); this.show_rpc_trace_for_server(&id, window, cx);
} }
} }
@ -690,31 +753,38 @@ impl DapLogView {
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> { fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
self.log_store self.log_store
.read(cx) .read(cx)
.projects
.get(&self.project.downgrade())
.map_or_else(Vec::new, |state| {
state
.debug_sessions .debug_sessions
.iter() .values()
.rev() .rev()
.map(|state| DapMenuItem { .map(|state| DapMenuItem {
session_id: state.id, session_id: state.id,
adapter_name: state.adapter_name.clone(), adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs, has_adapter_logs: state.has_adapter_logs,
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), selected_entry: self
.current_view
.map_or(LogKind::Adapter, |(_, kind)| kind),
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
})
} }
fn show_rpc_trace_for_server( fn show_rpc_trace_for_server(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let rpc_log = self.log_store.update(cx, |log_store, _| { let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store log_store
.rpc_messages_for_session(session_id) .rpc_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned())) .map(|state| log_contents(state.iter().cloned()))
}); });
if let Some(rpc_log) = rpc_log { if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc)); self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON"); let language = self.project.read(cx).languages().language_for_name("JSON");
editor editor
@ -725,8 +795,7 @@ impl DapLogView {
.expect("log buffer should be a singleton") .expect("log buffer should be a singleton")
.update(cx, |_, cx| { .update(cx, |_, cx| {
cx.spawn({ cx.spawn({
let buffer = cx.entity(); async move |buffer, cx| {
async move |_, cx| {
let language = language.await.ok(); let language = language.await.ok();
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx); buffer.set_language(language, cx);
@ -746,17 +815,17 @@ impl DapLogView {
fn show_log_messages_for_adapter( fn show_log_messages_for_adapter(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let message_log = self.log_store.update(cx, |log_store, _| { let message_log = self.log_store.update(cx, |log_store, _| {
log_store log_store
.log_messages_for_session(session_id) .log_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned())) .map(|state| log_contents(state.iter().cloned()))
}); });
if let Some(message_log) = message_log { if let Some(message_log) = message_log {
self.current_view = Some((session_id, LogKind::Adapter)); self.current_view = Some((id.session_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor editor
.read(cx) .read(cx)
@ -775,17 +844,17 @@ impl DapLogView {
fn show_initialization_sequence_for_server( fn show_initialization_sequence_for_server(
&mut self, &mut self,
session_id: SessionId, id: &LogStoreEntryIdentifier<'_>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let rpc_log = self.log_store.update(cx, |log_store, _| { let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store log_store
.initialization_sequence_for_session(session_id) .initialization_sequence_for_session(id)
.map(|state| log_contents(state.iter().cloned())) .map(|state| log_contents(state.iter().cloned()))
}); });
if let Some(rpc_log) = rpc_log { if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc)); self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON"); let language = self.project.read(cx).languages().language_for_name("JSON");
editor editor
@ -993,9 +1062,9 @@ impl Focusable for DapLogView {
} }
} }
pub enum Event { enum Event {
NewLogEntry { NewLogEntry {
id: SessionId, id: LogStoreEntryIdentifier<'static>,
entry: SharedString, entry: SharedString,
kind: LogKind, kind: LogKind,
}, },
@ -1008,31 +1077,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
impl LogStore { impl LogStore {
pub fn contained_session_ids(&self) -> Vec<SessionId> { pub fn has_projects(&self) -> bool {
self.debug_sessions !self.projects.is_empty()
.iter()
.map(|session| session.id)
.collect()
} }
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> { pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
self.debug_sessions self.projects.get(project).map_or(vec![], |state| {
.iter() state.debug_sessions.keys().copied().collect()
.find(|adapter_state| adapter_state.id == session_id) })
}
pub fn rpc_messages_for_session_id(
&self,
project: &WeakEntity<Project>,
session_id: SessionId,
) -> Vec<SharedString> {
self.projects.get(&project).map_or(vec![], |state| {
state
.debug_sessions
.get(&session_id)
.expect("This session should exist if a test is calling") .expect("This session should exist if a test is calling")
.rpc_messages .rpc_messages
.messages .messages
.clone() .clone()
.into() .into()
} })
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
.expect("This session should exist if a test is calling")
.log_messages
.clone()
.into()
} }
} }

View file

@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
.await; .await;
assert!( assert!(
log_store.read_with(cx, |log_store, _| log_store log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
.contained_session_ids() "log_store shouldn't contain any projects before any projects were created"
.is_empty()),
"log_store shouldn't contain any session IDs before any sessions were created"
); );
let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await; let workspace = init_test_workspace(&project, cx).await;
assert!(
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.is_empty()),
"log_store shouldn't contain any projects before any projects were created"
);
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session // Start a debug session
@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
let client = session.update(cx, |session, _| session.adapter_client().unwrap()); let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!( assert_eq!(
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()), log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.len()),
1, 1,
); );
assert!( assert!(
log_store.read_with(cx, |log_store, _| log_store log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids() .contained_session_ids(&project.downgrade())
.contains(&session_id)), .contains(&session_id)),
"log_store should contain the session IDs of the started session" "log_store should contain the session IDs of the started session"
); );
assert!( assert!(
!log_store.read_with(cx, |log_store, _| log_store !log_store.read_with(cx, |log_store, _| log_store
.rpc_messages_for_session_id(session_id) .rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()), .is_empty()),
"We should have the initialization sequence in the log store" "We should have the initialization sequence in the log store"
); );