597 lines
20 KiB
Rust
597 lines
20 KiB
Rust
use anyhow::Result;
|
|
use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus};
|
|
use dap::requests::{Disconnect, Scopes, StackTrace, Variables};
|
|
use dap::{client::DebugAdapterClient, transport::Events};
|
|
use dap::{
|
|
Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments,
|
|
StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
|
|
Variable, VariablesArguments,
|
|
};
|
|
use editor::Editor;
|
|
use futures::future::try_join_all;
|
|
use gpui::{
|
|
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
|
Subscription, Task, View, ViewContext, WeakView,
|
|
};
|
|
use std::path::Path;
|
|
use std::{collections::HashMap, sync::Arc};
|
|
use task::DebugRequestType;
|
|
use ui::prelude::*;
|
|
use workspace::Pane;
|
|
use workspace::{
|
|
dock::{DockPosition, Panel, PanelEvent},
|
|
Workspace,
|
|
};
|
|
|
|
use crate::debugger_panel_item::DebugPanelItem;
|
|
|
|
enum DebugCurrentRowHighlight {}
|
|
|
|
#[derive(Debug)]
|
|
pub enum DebugPanelEvent {
|
|
Stopped((DebugAdapterClientId, StoppedEvent)),
|
|
Thread((DebugAdapterClientId, ThreadEvent)),
|
|
}
|
|
|
|
actions!(debug_panel, [TogglePanel]);
|
|
|
|
pub struct DebugPanel {
|
|
size: Pixels,
|
|
pane: View<Pane>,
|
|
focus_handle: FocusHandle,
|
|
workspace: WeakView<Workspace>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl DebugPanel {
|
|
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
|
cx.new_view(|cx| {
|
|
let pane = cx.new_view(|cx| {
|
|
let mut pane = Pane::new(
|
|
workspace.weak_handle(),
|
|
workspace.project().clone(),
|
|
Default::default(),
|
|
None,
|
|
None,
|
|
cx,
|
|
);
|
|
pane.set_can_split(false, cx);
|
|
pane.set_can_navigate(true, cx);
|
|
pane.display_nav_history_buttons(None);
|
|
pane.set_should_display_tab_bar(|_| true);
|
|
|
|
pane
|
|
});
|
|
|
|
let project = workspace.project().clone();
|
|
|
|
let _subscriptions = vec![cx.subscribe(&project, {
|
|
move |this: &mut Self, _, event, cx| {
|
|
if let project::Event::DebugClientEvent { client_id, event } = event {
|
|
Self::handle_debug_client_events(this, client_id, event, cx);
|
|
}
|
|
}
|
|
})];
|
|
|
|
Self {
|
|
pane,
|
|
size: px(300.),
|
|
_subscriptions,
|
|
focus_handle: cx.focus_handle(),
|
|
workspace: workspace.weak_handle(),
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn load(
|
|
workspace: WeakView<Workspace>,
|
|
cx: AsyncWindowContext,
|
|
) -> Task<Result<View<Self>>> {
|
|
cx.spawn(|mut cx| async move {
|
|
workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
|
|
})
|
|
}
|
|
|
|
fn debug_client_by_id(
|
|
&self,
|
|
client_id: DebugAdapterClientId,
|
|
cx: &mut ViewContext<Self>,
|
|
) -> Arc<DebugAdapterClient> {
|
|
self.workspace
|
|
.update(cx, |this, cx| {
|
|
this.project()
|
|
.read(cx)
|
|
.debug_adapter_by_id(client_id)
|
|
.unwrap()
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
fn handle_debug_client_events(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
event: &Events,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
match event {
|
|
Events::Initialized(event) => {
|
|
Self::handle_initialized_event(this, client_id, event, cx)
|
|
}
|
|
Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx),
|
|
Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx),
|
|
Events::Exited(event) => Self::handle_exited_event(this, client_id, event, cx),
|
|
Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx),
|
|
Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx),
|
|
Events::Output(_) => {}
|
|
Events::Breakpoint(_) => {}
|
|
Events::Module(_) => {}
|
|
Events::LoadedSource(_) => {}
|
|
Events::Capabilities(_) => {}
|
|
Events::Memory(_) => {}
|
|
Events::Process(_) => {}
|
|
Events::ProgressEnd(_) => {}
|
|
Events::ProgressStart(_) => {}
|
|
Events::ProgressUpdate(_) => {}
|
|
Events::Invalidated(_) => {}
|
|
Events::Other(_) => {}
|
|
}
|
|
}
|
|
|
|
pub async fn go_to_stack_frame(
|
|
workspace: WeakView<Workspace>,
|
|
stack_frame: StackFrame,
|
|
client: Arc<DebugAdapterClient>,
|
|
clear_highlights: bool,
|
|
mut cx: AsyncWindowContext,
|
|
) -> Result<()> {
|
|
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
|
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
|
let column = (stack_frame.column.saturating_sub(1)) as u32;
|
|
|
|
if clear_highlights {
|
|
Self::remove_highlights(workspace.clone(), client, cx.clone()).await?;
|
|
}
|
|
|
|
let task = workspace.update(&mut cx, |workspace, cx| {
|
|
let project_path = workspace.project().read_with(cx, |project, cx| {
|
|
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
|
});
|
|
|
|
if let Some(project_path) = project_path {
|
|
workspace.open_path_preview(project_path, None, false, true, cx)
|
|
} else {
|
|
Task::ready(Err(anyhow::anyhow!(
|
|
"No project path found for path: {}",
|
|
path
|
|
)))
|
|
}
|
|
})?;
|
|
|
|
let editor = task.await?.downcast::<Editor>().unwrap();
|
|
|
|
workspace.update(&mut cx, |_, cx| {
|
|
editor.update(cx, |editor, cx| {
|
|
editor.go_to_line::<DebugCurrentRowHighlight>(
|
|
row,
|
|
column,
|
|
Some(cx.theme().colors().editor_highlighted_line_background),
|
|
cx,
|
|
);
|
|
})
|
|
})
|
|
}
|
|
|
|
async fn remove_highlights(
|
|
workspace: WeakView<Workspace>,
|
|
client: Arc<DebugAdapterClient>,
|
|
cx: AsyncWindowContext,
|
|
) -> Result<()> {
|
|
let mut tasks = Vec::new();
|
|
for thread_state in client.thread_states().values() {
|
|
for stack_frame in thread_state.stack_frames.clone() {
|
|
tasks.push(Self::remove_editor_highlight(
|
|
workspace.clone(),
|
|
stack_frame,
|
|
cx.clone(),
|
|
));
|
|
}
|
|
}
|
|
|
|
if !tasks.is_empty() {
|
|
try_join_all(tasks).await?;
|
|
}
|
|
|
|
anyhow::Ok(())
|
|
}
|
|
|
|
async fn remove_highlights_for_thread(
|
|
workspace: WeakView<Workspace>,
|
|
client: Arc<DebugAdapterClient>,
|
|
thread_id: u64,
|
|
cx: AsyncWindowContext,
|
|
) -> Result<()> {
|
|
let mut tasks = Vec::new();
|
|
if let Some(thread_state) = client.thread_states().get(&thread_id) {
|
|
for stack_frame in thread_state.stack_frames.clone() {
|
|
tasks.push(Self::remove_editor_highlight(
|
|
workspace.clone(),
|
|
stack_frame.clone(),
|
|
cx.clone(),
|
|
));
|
|
}
|
|
}
|
|
|
|
if !tasks.is_empty() {
|
|
try_join_all(tasks).await?;
|
|
}
|
|
|
|
anyhow::Ok(())
|
|
}
|
|
|
|
async fn remove_editor_highlight(
|
|
workspace: WeakView<Workspace>,
|
|
stack_frame: StackFrame,
|
|
mut cx: AsyncWindowContext,
|
|
) -> Result<()> {
|
|
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
|
|
|
let task = workspace.update(&mut cx, |workspace, cx| {
|
|
let project_path = workspace.project().read_with(cx, |project, cx| {
|
|
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
|
});
|
|
|
|
if let Some(project_path) = project_path {
|
|
workspace.open_path(project_path, None, false, cx)
|
|
} else {
|
|
Task::ready(Err(anyhow::anyhow!(
|
|
"No project path found for path: {}",
|
|
path
|
|
)))
|
|
}
|
|
})?;
|
|
|
|
let editor = task.await?.downcast::<Editor>().unwrap();
|
|
|
|
editor.update(&mut cx, |editor, _| {
|
|
editor.clear_row_highlights::<DebugCurrentRowHighlight>();
|
|
})
|
|
}
|
|
|
|
fn handle_initialized_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
_: &Option<Capabilities>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let client = this.debug_client_by_id(*client_id, cx);
|
|
|
|
cx.spawn(|this, mut cx| async move {
|
|
let task = this.update(&mut cx, |this, cx| {
|
|
this.workspace.update(cx, |workspace, cx| {
|
|
workspace.project().update(cx, |project, cx| {
|
|
let client = client.clone();
|
|
|
|
project.send_breakpoints(client, cx)
|
|
})
|
|
})
|
|
})??;
|
|
|
|
task.await?;
|
|
|
|
client.configuration_done().await
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
fn handle_continued_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
event: &ContinuedEvent,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let all_threads = event.all_threads_continued.unwrap_or(false);
|
|
let client = this.debug_client_by_id(*client_id, cx);
|
|
|
|
if all_threads {
|
|
for thread in client.thread_states().values_mut() {
|
|
thread.status = ThreadStatus::Running;
|
|
}
|
|
} else {
|
|
client.update_thread_state_status(event.thread_id, ThreadStatus::Running);
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn handle_stopped_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
event: &StoppedEvent,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let Some(thread_id) = event.thread_id else {
|
|
return;
|
|
};
|
|
|
|
let client_id = *client_id;
|
|
let client = this.debug_client_by_id(client_id, cx);
|
|
|
|
cx.spawn({
|
|
let event = event.clone();
|
|
|this, mut cx| async move {
|
|
let stack_trace_response = client
|
|
.request::<StackTrace>(StackTraceArguments {
|
|
thread_id,
|
|
start_frame: None,
|
|
levels: None,
|
|
format: None,
|
|
})
|
|
.await?;
|
|
|
|
let current_stack_frame =
|
|
stack_trace_response.stack_frames.first().unwrap().clone();
|
|
let mut scope_tasks = Vec::new();
|
|
for stack_frame in stack_trace_response.stack_frames.clone().into_iter() {
|
|
let frame_id = stack_frame.id;
|
|
let client = client.clone();
|
|
scope_tasks.push(async move {
|
|
anyhow::Ok((
|
|
frame_id,
|
|
client
|
|
.request::<Scopes>(ScopesArguments { frame_id })
|
|
.await?,
|
|
))
|
|
});
|
|
}
|
|
|
|
let mut scopes: HashMap<u64, Vec<Scope>> = HashMap::new();
|
|
let mut variables: HashMap<u64, Vec<Variable>> = HashMap::new();
|
|
|
|
let mut variable_tasks = Vec::new();
|
|
for (thread_id, response) in try_join_all(scope_tasks).await? {
|
|
scopes.insert(thread_id, response.scopes.clone());
|
|
|
|
for scope in response.scopes {
|
|
let scope_reference = scope.variables_reference;
|
|
let client = client.clone();
|
|
variable_tasks.push(async move {
|
|
anyhow::Ok((
|
|
scope_reference,
|
|
client
|
|
.request::<Variables>(VariablesArguments {
|
|
variables_reference: scope_reference,
|
|
filter: None,
|
|
start: None,
|
|
count: None,
|
|
format: None,
|
|
})
|
|
.await?,
|
|
))
|
|
});
|
|
}
|
|
}
|
|
|
|
for (scope_reference, response) in try_join_all(variable_tasks).await? {
|
|
variables.insert(scope_reference, response.variables.clone());
|
|
}
|
|
|
|
this.update(&mut cx, |this, cx| {
|
|
let mut thread_state = client.thread_states();
|
|
let thread_state = thread_state
|
|
.entry(thread_id)
|
|
.or_insert(ThreadState::default());
|
|
|
|
thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id);
|
|
thread_state.stack_frames = stack_trace_response.stack_frames;
|
|
thread_state.scopes = scopes;
|
|
thread_state.variables = variables;
|
|
thread_state.status = ThreadStatus::Stopped;
|
|
|
|
let existing_item = this
|
|
.pane
|
|
.read(cx)
|
|
.items()
|
|
.filter_map(|item| item.downcast::<DebugPanelItem>())
|
|
.any(|item| {
|
|
let item = item.read(cx);
|
|
|
|
item.client().id() == client_id && item.thread_id() == thread_id
|
|
});
|
|
|
|
if !existing_item {
|
|
let debug_panel = cx.view().clone();
|
|
this.pane.update(cx, |this, cx| {
|
|
let tab = cx.new_view(|cx| {
|
|
DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx)
|
|
});
|
|
|
|
this.add_item(Box::new(tab.clone()), false, false, None, cx)
|
|
});
|
|
}
|
|
|
|
cx.emit(DebugPanelEvent::Stopped((client_id, event)));
|
|
|
|
if let Some(item) = this.pane.read(cx).active_item() {
|
|
if let Some(pane) = item.downcast::<DebugPanelItem>() {
|
|
if pane.read(cx).thread_id() == thread_id {
|
|
let workspace = this.workspace.clone();
|
|
let client = client.clone();
|
|
return cx.spawn(|_, cx| async move {
|
|
Self::go_to_stack_frame(
|
|
workspace,
|
|
current_stack_frame.clone(),
|
|
client,
|
|
true,
|
|
cx,
|
|
)
|
|
.await
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Task::ready(anyhow::Ok(()))
|
|
})?
|
|
.await
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
fn handle_thread_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
event: &ThreadEvent,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let client = this.debug_client_by_id(*client_id, cx);
|
|
let thread_id = event.thread_id;
|
|
|
|
if event.reason == ThreadEventReason::Started {
|
|
client
|
|
.thread_states()
|
|
.insert(thread_id, ThreadState::default());
|
|
} else {
|
|
client.update_thread_state_status(thread_id, ThreadStatus::Ended);
|
|
|
|
// TODO: we want to figure out for witch clients/threads we should remove the highlights
|
|
cx.spawn({
|
|
let client = client.clone();
|
|
|this, mut cx| async move {
|
|
let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?;
|
|
|
|
Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?;
|
|
|
|
anyhow::Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
cx.emit(DebugPanelEvent::Thread((*client_id, event.clone())));
|
|
}
|
|
|
|
fn handle_exited_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
_: &ExitedEvent,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let client = this.debug_client_by_id(*client_id, cx);
|
|
cx.spawn(|_, _| async move {
|
|
for thread_state in client.thread_states().values_mut() {
|
|
thread_state.status = ThreadStatus::Exited;
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn handle_terminated_event(
|
|
this: &mut Self,
|
|
client_id: &DebugAdapterClientId,
|
|
event: &Option<TerminatedEvent>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let restart_args = event.clone().and_then(|e| e.restart);
|
|
let client = this.debug_client_by_id(*client_id, cx);
|
|
let workspace = this.workspace.clone();
|
|
|
|
cx.spawn(|_, cx| async move {
|
|
let should_restart = restart_args.is_some();
|
|
|
|
Self::remove_highlights(workspace, client.clone(), cx).await?;
|
|
|
|
client
|
|
.request::<Disconnect>(DisconnectArguments {
|
|
restart: Some(should_restart),
|
|
terminate_debuggee: None,
|
|
suspend_debuggee: None,
|
|
})
|
|
.await?;
|
|
|
|
if should_restart {
|
|
match client.request_type() {
|
|
DebugRequestType::Launch => client.launch(restart_args).await,
|
|
DebugRequestType::Attach => client.attach(restart_args).await,
|
|
}
|
|
} else {
|
|
anyhow::Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<PanelEvent> for DebugPanel {}
|
|
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
|
impl EventEmitter<project::Event> for DebugPanel {}
|
|
|
|
impl FocusableView for DebugPanel {
|
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Panel for DebugPanel {
|
|
fn persistent_name() -> &'static str {
|
|
"DebugPanel"
|
|
}
|
|
|
|
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
|
DockPosition::Bottom
|
|
}
|
|
|
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
|
position == DockPosition::Bottom
|
|
}
|
|
|
|
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
|
|
|
fn size(&self, _cx: &WindowContext) -> Pixels {
|
|
self.size
|
|
}
|
|
|
|
fn set_size(&mut self, size: Option<Pixels>, _cx: &mut ViewContext<Self>) {
|
|
self.size = size.unwrap();
|
|
}
|
|
|
|
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
|
None
|
|
}
|
|
|
|
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
fn toggle_action(&self) -> Box<dyn Action> {
|
|
Box::new(TogglePanel)
|
|
}
|
|
|
|
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
|
false
|
|
}
|
|
|
|
fn starts_open(&self, _cx: &WindowContext) -> bool {
|
|
false
|
|
}
|
|
|
|
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
|
|
|
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
|
}
|
|
|
|
impl Render for DebugPanel {
|
|
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.key_context("DebugPanel")
|
|
.track_focus(&self.focus_handle)
|
|
.size_full()
|
|
.child(self.pane.clone())
|
|
.into_any()
|
|
}
|
|
}
|