Debugger implementation (#13433)
### DISCLAIMER > As of 6th March 2025, debugger is still in development. We plan to merge it behind a staff-only feature flag for staff use only, followed by non-public release and then finally a public one (akin to how Git panel release was handled). This is done to ensure the best experience when it gets released. ### END OF DISCLAIMER **The current state of the debugger implementation:** https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9 https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f ---- All the todo's are in the following channel, so it's easier to work on this together: https://zed.dev/channel/zed-debugger-11370 If you are on Linux, you can use the following command to join the channel: ```cli zed https://zed.dev/channel/zed-debugger-11370 ``` ## Current Features - Collab - Breakpoints - Sync when you (re)join a project - Sync when you add/remove a breakpoint - Sync active debug line - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Restart stack frame (if adapter supports this) - Variables - Loaded sources - Modules - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Debug console - Breakpoints - Log breakpoints - line breakpoints - Persistent between zed sessions (configurable) - Multi buffer support - Toggle disable/enable all breakpoints - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Show collapsed stack frames - Restart stack frame (if adapter supports this) - Loaded sources - View all used loaded sources if supported by adapter. - Modules - View all used modules (if adapter supports this) - Variables - Copy value - Copy name - Copy memory reference - Set value (if adapter supports this) - keyboard navigation - Debug Console - See logs - View output that was sent from debug adapter - Output grouping - Evaluate code - Updates the variable list - Auto completion - If not supported by adapter, we will show auto-completion for existing variables - Debug Terminal - Run custom commands and change env values right inside your Zed terminal - Attach to process (if adapter supports this) - Process picker - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Disconnect - Restart - Stop - Warning when a debug session exited without hitting any breakpoint - Debug view to see Adapter/RPC log messages - Testing - Fake debug adapter - Fake requests & events --- Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com> Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
26
crates/debugger_tools/Cargo.toml
Normal file
26
crates/debugger_tools/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "debugger_tools"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/debugger_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
1
crates/debugger_tools/LICENSE-GPL
Symbolic link
1
crates/debugger_tools/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
845
crates/debugger_tools/src/dap_log.rs
Normal file
845
crates/debugger_tools/src/dap_log.rs
Normal file
|
@ -0,0 +1,845 @@
|
|||
use dap::{
|
||||
client::SessionId,
|
||||
debugger_settings::DebuggerSettings,
|
||||
transport::{IoKind, LogKind},
|
||||
};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedSender},
|
||||
StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||
};
|
||||
use project::{
|
||||
debugger::{dap_store, session::Session},
|
||||
search::SearchQuery,
|
||||
Project,
|
||||
};
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::maybe;
|
||||
use workspace::{
|
||||
item::Item,
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ui::{h_flex, Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu},
|
||||
ToolbarItemEvent, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_clients: HashMap<SessionId, DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
struct DebugAdapterState {
|
||||
log_messages: VecDeque<String>,
|
||||
rpc_messages: RpcMessages,
|
||||
}
|
||||
|
||||
struct RpcMessages {
|
||||
messages: VecDeque<String>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
}
|
||||
|
||||
impl RpcMessages {
|
||||
const MESSAGE_QUEUE_LIMIT: usize = 255;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_message_kind: None,
|
||||
messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SEND: &str = "// Send";
|
||||
const RECEIVE: &str = "// Receive";
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum MessageKind {
|
||||
Send,
|
||||
Receive,
|
||||
}
|
||||
|
||||
impl MessageKind {
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Send => SEND,
|
||||
Self::Receive => RECEIVE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugAdapterState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
log_messages: VecDeque::new(),
|
||||
rpc_messages: RpcMessages::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_rpc_log(client_id, io_kind, &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, String)>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_adapter_log(client_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_clients: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_rpc_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
fn on_adapter_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
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_client(*session_id, session, cx);
|
||||
}
|
||||
}
|
||||
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
|
||||
this.remove_debug_client(*session_id, cx);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
|
||||
self.debug_clients.get_mut(&id)
|
||||
}
|
||||
|
||||
fn add_debug_client_message(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let kind = match io_kind {
|
||||
IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
|
||||
IoKind::StdIn => MessageKind::Send,
|
||||
};
|
||||
|
||||
let rpc_messages = &mut debug_client_state.rpc_messages;
|
||||
if rpc_messages.last_message_kind != Some(kind) {
|
||||
Self::add_debug_client_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
kind.label().to_string(),
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
);
|
||||
rpc_messages.last_message_kind = Some(kind);
|
||||
}
|
||||
Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_debug_client_log(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
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 debug_client_state.log_messages,
|
||||
id,
|
||||
message,
|
||||
LogKind::Adapter,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_debug_client_entry(
|
||||
log_lines: &mut VecDeque<String>,
|
||||
id: SessionId,
|
||||
message: String,
|
||||
kind: LogKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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()
|
||||
})
|
||||
.unwrap_or(message)
|
||||
} else {
|
||||
message
|
||||
};
|
||||
log_lines.push_back(entry.clone());
|
||||
|
||||
cx.emit(Event::NewLogEntry { id, entry, kind });
|
||||
}
|
||||
|
||||
fn add_debug_client(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
client: Entity<Session>,
|
||||
cx: &App,
|
||||
) -> Option<&mut DebugAdapterState> {
|
||||
let client_state = self
|
||||
.debug_clients
|
||||
.entry(client_id)
|
||||
.or_insert_with(DebugAdapterState::new);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
client.add_log_handler(
|
||||
move |io_kind, message| {
|
||||
io_tx
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
client.add_log_handler(
|
||||
move |io_kind, message| {
|
||||
log_io_tx
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
|
||||
Some(client_state)
|
||||
}
|
||||
|
||||
fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
|
||||
self.debug_clients.remove(&client_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
|
||||
}
|
||||
|
||||
fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(
|
||||
&mut self
|
||||
.debug_clients
|
||||
.get_mut(&client_id)?
|
||||
.rpc_messages
|
||||
.messages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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_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| {
|
||||
menu_rows
|
||||
.iter()
|
||||
.find(|row| row.client_id == current_client_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.client_name,
|
||||
sub_item.client_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.client_id.0, row.client_name,),
|
||||
)
|
||||
.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, move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(row.client_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, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.client_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 } => {
|
||||
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,
|
||||
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) -> Option<Vec<DapMenuItem>> {
|
||||
let mut menu_items = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.filter_map(|client| {
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
Some(DapMenuItem {
|
||||
client_id: client.id(),
|
||||
client_name: client.name().0.as_ref().into(),
|
||||
has_adapter_logs: client.has_adapter_logs(),
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
menu_items.sort_by_key(|item| item.client_id.0);
|
||||
Some(menu_items)
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.rpc_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((client_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, 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();
|
||||
|_, 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(window);
|
||||
}
|
||||
|
||||
fn show_log_messages_for_adapter(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let message_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.log_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((client_id, LogKind::Adapter));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, 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 log_contents(lines: &VecDeque<String>) -> String {
|
||||
let (a, b) = lines.as_slices();
|
||||
let a = a.iter().map(move |v| v.as_ref());
|
||||
let b = b.iter().map(move |v| v.as_ref());
|
||||
a.chain(b).fold(String::new(), |mut acc, el| {
|
||||
acc.push_str(el);
|
||||
acc.push('\n');
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct DapMenuItem {
|
||||
pub client_id: SessionId,
|
||||
pub client_name: String,
|
||||
pub has_adapter_logs: bool,
|
||||
pub selected_entry: LogKind,
|
||||
}
|
||||
|
||||
const ADAPTER_LOGS: &str = "Adapter Logs";
|
||||
const RPC_MESSAGES: &str = "RPC Messages";
|
||||
|
||||
impl Render for DapLogView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.render(window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
actions!(debug, [OpenDebuggerAdapterLogs]);
|
||||
|
||||
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, _: &OpenDebuggerAdapterLogs, 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, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("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()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
NewLogEntry {
|
||||
id: SessionId,
|
||||
entry: String,
|
||||
kind: LogKind,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for DapLogView {}
|
||||
impl EventEmitter<EditorEvent> for DapLogView {}
|
||||
impl EventEmitter<SearchEvent> for DapLogView {}
|
8
crates/debugger_tools/src/debugger_tools.rs
Normal file
8
crates/debugger_tools/src/debugger_tools.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
mod dap_log;
|
||||
pub use dap_log::*;
|
||||
|
||||
use gpui::App;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
dap_log::init(cx);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue