
This PR updates the debugger panel's session list to be more useful in some cases that are commonly hit when using the JavaScript adapter. We make two adjustments, which only apply to JavaScript sessions: - For a child session that's the only child of a root session, we collapse it with its parent. This imitates what VS Code does in the "call stack" view for JavaScript sessions. - When a session has exactly one thread, we label the session with that thread's name, instead of the session label provided by the DAP. VS Code also makes this adjustment, which surfaces more useful information when working with browser sessions. Closes #33072 Release Notes: - debugger: Improved the appearance of JavaScript sessions in the debug panel's session list. --------- Co-authored-by: Julia <julia@zed.dev> Co-authored-by: Remco Smits <djsmits12@gmail.com>
1145 lines
37 KiB
Rust
1145 lines
37 KiB
Rust
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::{BTreeMap, 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},
|
|
};
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
enum View {
|
|
AdapterLogs,
|
|
RpcMessages,
|
|
InitializationSequence,
|
|
}
|
|
|
|
struct DapLogView {
|
|
editor: Entity<Editor>,
|
|
focus_handle: FocusHandle,
|
|
log_store: Entity<LogStore>,
|
|
editor_subscriptions: Vec<Subscription>,
|
|
current_view: Option<(SessionId, View)>,
|
|
project: Entity<Project>,
|
|
_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 {
|
|
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
|
rpc_tx: UnboundedSender<LogStoreMessage>,
|
|
adapter_log_tx: UnboundedSender<LogStoreMessage>,
|
|
}
|
|
|
|
struct ProjectState {
|
|
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
|
|
_subscriptions: [gpui::Subscription; 2],
|
|
}
|
|
|
|
struct DebugAdapterState {
|
|
id: SessionId,
|
|
log_messages: VecDeque<SharedString>,
|
|
rpc_messages: RpcMessages,
|
|
session_label: SharedString,
|
|
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,
|
|
session_label: SharedString,
|
|
has_adapter_logs: bool,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
log_messages: VecDeque::new(),
|
|
rpc_messages: RpcMessages::new(),
|
|
adapter_name,
|
|
session_label,
|
|
has_adapter_logs,
|
|
is_terminated: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LogStore {
|
|
pub fn new(cx: &Context<Self>) -> Self {
|
|
let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
|
|
cx.spawn(async move |this, cx| {
|
|
while let Some(message) = rpc_rx.next().await {
|
|
if let Some(this) = this.upgrade() {
|
|
this.update(cx, |this, cx| {
|
|
this.add_debug_adapter_message(message, cx);
|
|
})?;
|
|
}
|
|
|
|
smol::future::yield_now().await;
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
|
|
cx.spawn(async move |this, cx| {
|
|
while let Some(message) = adapter_log_rx.next().await {
|
|
if let Some(this) = this.upgrade() {
|
|
this.update(cx, |this, cx| {
|
|
this.add_debug_adapter_log(message, cx);
|
|
})?;
|
|
}
|
|
|
|
smol::future::yield_now().await;
|
|
}
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
Self {
|
|
rpc_tx,
|
|
adapter_log_tx,
|
|
projects: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
|
|
self.projects.insert(
|
|
project.downgrade(),
|
|
ProjectState {
|
|
_subscriptions: [
|
|
cx.observe_release(project, {
|
|
let weak_project = project.downgrade();
|
|
move |this, _, _| {
|
|
this.projects.remove(&weak_project);
|
|
}
|
|
}),
|
|
cx.subscribe(&project.read(cx).dap_store(), {
|
|
let weak_project = project.downgrade();
|
|
move |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(
|
|
LogStoreEntryIdentifier {
|
|
project: Cow::Owned(weak_project.clone()),
|
|
session_id: *session_id,
|
|
},
|
|
session,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
|
|
let id = LogStoreEntryIdentifier {
|
|
project: Cow::Borrowed(&weak_project),
|
|
session_id: *session_id,
|
|
};
|
|
if let Some(state) = this.get_debug_adapter_state(&id) {
|
|
state.is_terminated = true;
|
|
}
|
|
|
|
this.clean_sessions(cx);
|
|
}
|
|
_ => {}
|
|
}
|
|
}),
|
|
],
|
|
debug_sessions: Default::default(),
|
|
},
|
|
);
|
|
}
|
|
|
|
fn get_debug_adapter_state(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
) -> 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(
|
|
&mut self,
|
|
LogStoreMessage {
|
|
id,
|
|
kind: io_kind,
|
|
command,
|
|
message,
|
|
}: LogStoreMessage,
|
|
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.to_owned(),
|
|
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.to_owned(),
|
|
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,
|
|
LogStoreMessage {
|
|
id,
|
|
kind: io_kind,
|
|
message,
|
|
..
|
|
}: LogStoreMessage,
|
|
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.to_owned(),
|
|
message,
|
|
LogKind::Adapter,
|
|
cx,
|
|
);
|
|
cx.notify();
|
|
}
|
|
|
|
fn get_debug_adapter_entry(
|
|
log_lines: &mut VecDeque<SharedString>,
|
|
id: LogStoreEntryIdentifier<'static>,
|
|
message: SharedString,
|
|
kind: LogKind,
|
|
cx: &mut Context<Self>,
|
|
) -> SharedString {
|
|
if let Some(excess) = log_lines
|
|
.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 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,
|
|
id: LogStoreEntryIdentifier<'static>,
|
|
session: Entity<Session>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
maybe!({
|
|
let project_entry = self.projects.get_mut(&id.project)?;
|
|
let std::collections::btree_map::Entry::Vacant(state) =
|
|
project_entry.debug_sessions.entry(id.session_id)
|
|
else {
|
|
return None;
|
|
};
|
|
|
|
let (adapter_name, session_label, has_adapter_logs) =
|
|
session.read_with(cx, |session, _| {
|
|
(
|
|
session.adapter(),
|
|
session.label(),
|
|
session
|
|
.adapter_client()
|
|
.map_or(false, |client| client.has_adapter_logs()),
|
|
)
|
|
});
|
|
|
|
state.insert(DebugAdapterState::new(
|
|
id.session_id,
|
|
adapter_name,
|
|
session_label
|
|
.unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
|
|
has_adapter_logs,
|
|
));
|
|
|
|
self.clean_sessions(cx);
|
|
|
|
let io_tx = self.rpc_tx.clone();
|
|
|
|
let client = session.read(cx).adapter_client()?;
|
|
let project = id.project.clone();
|
|
let session_id = id.session_id;
|
|
client.add_log_handler(
|
|
move |kind, command, message| {
|
|
io_tx
|
|
.unbounded_send(LogStoreMessage {
|
|
id: LogStoreEntryIdentifier {
|
|
session_id,
|
|
project: project.clone(),
|
|
},
|
|
kind,
|
|
command: command.map(|command| command.to_owned().into()),
|
|
message: message.to_owned().into(),
|
|
})
|
|
.ok();
|
|
},
|
|
LogKind::Rpc,
|
|
);
|
|
|
|
let log_io_tx = self.adapter_log_tx.clone();
|
|
let project = id.project;
|
|
client.add_log_handler(
|
|
move |kind, command, message| {
|
|
log_io_tx
|
|
.unbounded_send(LogStoreMessage {
|
|
id: LogStoreEntryIdentifier {
|
|
session_id,
|
|
project: project.clone(),
|
|
},
|
|
kind,
|
|
command: command.map(|command| command.to_owned().into()),
|
|
message: message.to_owned().into(),
|
|
})
|
|
.ok();
|
|
},
|
|
LogKind::Adapter,
|
|
);
|
|
Some(())
|
|
});
|
|
}
|
|
|
|
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
|
|
self.projects.values_mut().for_each(|project| {
|
|
let mut allowed_terminated_sessions = 10u32;
|
|
project.debug_sessions.retain(|_, session| {
|
|
if !session.is_terminated {
|
|
return true;
|
|
}
|
|
allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
|
|
allowed_terminated_sessions > 0
|
|
});
|
|
});
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn log_messages_for_session(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
) -> Option<&mut VecDeque<SharedString>> {
|
|
self.get_debug_adapter_state(id)
|
|
.map(|state| &mut state.log_messages)
|
|
}
|
|
|
|
fn rpc_messages_for_session(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
) -> Option<&mut VecDeque<SharedString>> {
|
|
self.get_debug_adapter_state(id)
|
|
.map(|state| &mut state.rpc_messages.messages)
|
|
}
|
|
|
|
fn initialization_sequence_for_session(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
) -> Option<&Vec<SharedString>> {
|
|
self.get_debug_adapter_state(&id)
|
|
.map(|state| &state.rpc_messages.initialization_sequence)
|
|
}
|
|
}
|
|
|
|
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, project) = log_view.update(cx, |log_view, cx| {
|
|
(
|
|
log_view.menu_items(cx),
|
|
log_view.current_view.map(|(session_id, _)| session_id),
|
|
log_view.project.downgrade(),
|
|
)
|
|
});
|
|
|
|
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_label,
|
|
match sub_item.selected_entry {
|
|
View::AdapterLogs => ADAPTER_LOGS,
|
|
View::RpcMessages => RPC_MESSAGES,
|
|
View::InitializationSequence => INITIALIZATION_SEQUENCE,
|
|
}
|
|
))
|
|
})
|
|
.unwrap_or_else(|| "No adapter selected".into()),
|
|
))
|
|
.menu(move |mut window, cx| {
|
|
let log_view = log_view.clone();
|
|
let menu_rows = menu_rows.clone();
|
|
let project = project.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.adapter_name, row.session_label
|
|
))
|
|
.color(workspace::ui::Color::Muted),
|
|
)
|
|
.into_any_element()
|
|
});
|
|
|
|
if row.has_adapter_logs {
|
|
menu = menu.custom_entry(
|
|
move |_window, _cx| {
|
|
div()
|
|
.w_full()
|
|
.pl_4()
|
|
.child(Label::new(ADAPTER_LOGS))
|
|
.into_any_element()
|
|
},
|
|
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_log_messages_for_adapter(&id, window, cx);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
menu = menu
|
|
.custom_entry(
|
|
move |_window, _cx| {
|
|
div()
|
|
.w_full()
|
|
.pl_4()
|
|
.child(Label::new(RPC_MESSAGES))
|
|
.into_any_element()
|
|
},
|
|
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_rpc_trace_for_server(&id, window, cx);
|
|
}
|
|
}),
|
|
)
|
|
.custom_entry(
|
|
move |_window, _cx| {
|
|
div()
|
|
.w_full()
|
|
.pl_4()
|
|
.child(Label::new(INITIALIZATION_SEQUENCE))
|
|
.into_any_element()
|
|
},
|
|
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(
|
|
&id, window, cx,
|
|
);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
menu
|
|
})
|
|
.into()
|
|
});
|
|
|
|
h_flex()
|
|
.size_full()
|
|
.child(dap_menu)
|
|
.child(
|
|
div()
|
|
.child(
|
|
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|
|
|this, _, window, 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(window, cx);
|
|
editor.set_read_only(true);
|
|
});
|
|
})
|
|
}
|
|
},
|
|
)),
|
|
)
|
|
.ml_2(),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
|
|
|
|
impl ToolbarItemView for DapLogToolbarItemView {
|
|
fn set_active_pane_item(
|
|
&mut self,
|
|
active_pane_item: Option<&dyn workspace::item::ItemHandle>,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> workspace::ToolbarItemLocation {
|
|
if let Some(item) = active_pane_item {
|
|
if let Some(log_view) = item.downcast::<DapLogView>() {
|
|
self.log_view = Some(log_view.clone());
|
|
return workspace::ToolbarItemLocation::PrimaryLeft;
|
|
}
|
|
}
|
|
self.log_view = None;
|
|
|
|
cx.notify();
|
|
|
|
workspace::ToolbarItemLocation::Hidden
|
|
}
|
|
}
|
|
|
|
impl DapLogView {
|
|
pub fn new(
|
|
project: Entity<Project>,
|
|
log_store: Entity<LogStore>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
|
|
|
|
let focus_handle = cx.focus_handle();
|
|
|
|
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
|
Event::NewLogEntry { id, entry, kind } => {
|
|
let is_current_view = match (log_view.current_view, *kind) {
|
|
(Some((i, View::AdapterLogs)), LogKind::Adapter)
|
|
| (Some((i, View::RpcMessages)), LogKind::Rpc)
|
|
if i == id.session_id =>
|
|
{
|
|
log_view.project == *id.project
|
|
}
|
|
_ => false,
|
|
};
|
|
if is_current_view {
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
let weak_project = project.downgrade();
|
|
let state_info = log_store
|
|
.read(cx)
|
|
.projects
|
|
.get(&weak_project)
|
|
.and_then(|project| {
|
|
project
|
|
.debug_sessions
|
|
.values()
|
|
.next_back()
|
|
.map(|session| (session.id, session.has_adapter_logs))
|
|
});
|
|
|
|
let mut this = Self {
|
|
editor,
|
|
focus_handle,
|
|
project,
|
|
log_store,
|
|
editor_subscriptions,
|
|
current_view: None,
|
|
_subscriptions: vec![events_subscriptions],
|
|
};
|
|
|
|
if let Some((session_id, have_adapter_logs)) = state_info {
|
|
let id = LogStoreEntryIdentifier {
|
|
session_id,
|
|
project: Cow::Owned(weak_project),
|
|
};
|
|
if have_adapter_logs {
|
|
this.show_log_messages_for_adapter(&id, window, cx);
|
|
} else {
|
|
this.show_rpc_trace_for_server(&id, window, cx);
|
|
}
|
|
}
|
|
|
|
this
|
|
}
|
|
|
|
fn editor_for_logs(
|
|
log_contents: String,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> (Entity<Editor>, Vec<Subscription>) {
|
|
let editor = cx.new(|cx| {
|
|
let mut editor = Editor::multi_line(window, cx);
|
|
editor.set_text(log_contents, window, cx);
|
|
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
|
editor.set_show_code_actions(false, cx);
|
|
editor.set_show_breakpoints(false, cx);
|
|
editor.set_show_git_diff_gutter(false, cx);
|
|
editor.set_show_runnables(false, cx);
|
|
editor.set_input_enabled(false);
|
|
editor.set_use_autoclose(false);
|
|
editor.set_read_only(true);
|
|
editor.set_show_edit_predictions(Some(false), window, cx);
|
|
editor
|
|
});
|
|
let editor_subscription = cx.subscribe(
|
|
&editor,
|
|
|_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
|
|
);
|
|
let search_subscription = cx.subscribe(
|
|
&editor,
|
|
|_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
|
|
);
|
|
(editor, vec![editor_subscription, search_subscription])
|
|
}
|
|
|
|
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
|
|
self.log_store
|
|
.read(cx)
|
|
.projects
|
|
.get(&self.project.downgrade())
|
|
.map_or_else(Vec::new, |state| {
|
|
state
|
|
.debug_sessions
|
|
.values()
|
|
.rev()
|
|
.map(|state| DapMenuItem {
|
|
session_id: state.id,
|
|
adapter_name: state.adapter_name.clone(),
|
|
session_label: state.session_label.clone(),
|
|
has_adapter_logs: state.has_adapter_logs,
|
|
selected_entry: self
|
|
.current_view
|
|
.map_or(View::AdapterLogs, |(_, kind)| kind),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
}
|
|
|
|
fn show_rpc_trace_for_server(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
|
log_store
|
|
.rpc_messages_for_session(id)
|
|
.map(|state| log_contents(state.iter().cloned()))
|
|
});
|
|
if let Some(rpc_log) = rpc_log {
|
|
self.current_view = Some((id.session_id, View::RpcMessages));
|
|
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, 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({
|
|
async move |buffer, cx| {
|
|
let language = language.await.ok();
|
|
buffer.update(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(window);
|
|
}
|
|
|
|
fn show_log_messages_for_adapter(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let message_log = self.log_store.update(cx, |log_store, _| {
|
|
log_store
|
|
.log_messages_for_session(id)
|
|
.map(|state| log_contents(state.iter().cloned()))
|
|
});
|
|
if let Some(message_log) = message_log {
|
|
self.current_view = Some((id.session_id, View::AdapterLogs));
|
|
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, 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(window);
|
|
}
|
|
|
|
fn show_initialization_sequence_for_server(
|
|
&mut self,
|
|
id: &LogStoreEntryIdentifier<'_>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
|
log_store
|
|
.initialization_sequence_for_session(id)
|
|
.map(|state| log_contents(state.iter().cloned()))
|
|
});
|
|
if let Some(rpc_log) = rpc_log {
|
|
self.current_view = Some((id.session_id, View::InitializationSequence));
|
|
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, 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.entity();
|
|
async move |_, cx| {
|
|
let language = language.await.ok();
|
|
buffer.update(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(window);
|
|
}
|
|
}
|
|
|
|
fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
|
|
lines.fold(String::new(), |mut acc, el| {
|
|
acc.push_str(&el);
|
|
acc.push('\n');
|
|
acc
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
struct DapMenuItem {
|
|
session_id: SessionId,
|
|
session_label: SharedString,
|
|
adapter_name: DebugAdapterName,
|
|
has_adapter_logs: bool,
|
|
selected_entry: View,
|
|
}
|
|
|
|
const ADAPTER_LOGS: &str = "Adapter Logs";
|
|
const RPC_MESSAGES: &str = "RPC Messages";
|
|
const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
|
|
|
|
impl Render for DapLogView {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.render(window, cx).into_any_element()
|
|
})
|
|
}
|
|
}
|
|
|
|
actions!(
|
|
dev,
|
|
[
|
|
/// Opens the debug adapter protocol logs viewer.
|
|
OpenDebugAdapterLogs
|
|
]
|
|
);
|
|
|
|
pub fn init(cx: &mut App) {
|
|
let log_store = cx.new(|cx| LogStore::new(cx));
|
|
|
|
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
|
let Some(_window) = window else {
|
|
return;
|
|
};
|
|
|
|
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, _: &OpenDebugAdapterLogs, window, cx| {
|
|
let project = workspace.project().read(cx);
|
|
if project.is_local() {
|
|
workspace.add_item_to_active_pane(
|
|
Box::new(cx.new(|cx| {
|
|
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
|
|
})),
|
|
None,
|
|
true,
|
|
window,
|
|
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, _detail: usize, _cx: &App) -> SharedString {
|
|
"DAP Logs".into()
|
|
}
|
|
|
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
|
Some(Box::new(handle.clone()))
|
|
}
|
|
}
|
|
|
|
impl SearchableItem for DapLogView {
|
|
type Match = <Editor as SearchableItem>::Match;
|
|
|
|
fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
|
|
}
|
|
|
|
fn update_matches(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor
|
|
.update(cx, |e, cx| e.update_matches(matches, window, cx))
|
|
}
|
|
|
|
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
|
|
self.editor
|
|
.update(cx, |e, cx| e.query_suggestion(window, cx))
|
|
}
|
|
|
|
fn activate_match(
|
|
&mut self,
|
|
index: usize,
|
|
matches: &[Self::Match],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor
|
|
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
|
|
}
|
|
|
|
fn select_matches(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor
|
|
.update(cx, |e, cx| e.select_matches(matches, window, cx))
|
|
}
|
|
|
|
fn find_matches(
|
|
&mut self,
|
|
query: Arc<project::search::SearchQuery>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<Vec<Self::Match>> {
|
|
self.editor
|
|
.update(cx, |e, cx| e.find_matches(query, window, cx))
|
|
}
|
|
|
|
fn replace(
|
|
&mut self,
|
|
_: &Self::Match,
|
|
_: &SearchQuery,
|
|
_window: &mut Window,
|
|
_: &mut Context<Self>,
|
|
) {
|
|
// Since DAP Log is read-only, it doesn't make sense to support replace operation.
|
|
}
|
|
|
|
fn supported_options(&self) -> workspace::searchable::SearchOptions {
|
|
workspace::searchable::SearchOptions {
|
|
case: true,
|
|
word: true,
|
|
regex: true,
|
|
find_in_results: true,
|
|
// DAP log is read-only.
|
|
replacement: false,
|
|
selection: false,
|
|
}
|
|
}
|
|
fn active_match_index(
|
|
&mut self,
|
|
direction: Direction,
|
|
matches: &[Self::Match],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<usize> {
|
|
self.editor.update(cx, |e, cx| {
|
|
e.active_match_index(direction, matches, window, cx)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Focusable for DapLogView {
|
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
enum Event {
|
|
NewLogEntry {
|
|
id: LogStoreEntryIdentifier<'static>,
|
|
entry: SharedString,
|
|
kind: LogKind,
|
|
},
|
|
}
|
|
|
|
impl EventEmitter<Event> for LogStore {}
|
|
impl EventEmitter<Event> for DapLogView {}
|
|
impl EventEmitter<EditorEvent> for DapLogView {}
|
|
impl EventEmitter<SearchEvent> for DapLogView {}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
impl LogStore {
|
|
pub fn has_projects(&self) -> bool {
|
|
!self.projects.is_empty()
|
|
}
|
|
|
|
pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
|
|
self.projects.get(project).map_or(vec![], |state| {
|
|
state.debug_sessions.keys().copied().collect()
|
|
})
|
|
}
|
|
|
|
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")
|
|
.rpc_messages
|
|
.messages
|
|
.clone()
|
|
.into()
|
|
})
|
|
}
|
|
}
|