Skip to content

Commit

Permalink
Debug terminal (#50)
Browse files Browse the repository at this point in the history
* Make program optional in debug task format

* Remove default config for skipFile JavaScript debugger

* Add Debug case to TerminalKind

* Don't allow serializing debug terminals

* Add respond method so we can send response back for reverse requests

* Implement run in terminal reverse request

* Move client calls to dap store

This commit also fixes an issue with not sending a response for the `StartDebugging` reverse request.

* Make clippy happy
  • Loading branch information
RemcoSmitsDev authored Oct 17, 2024
1 parent 3a6f2ad commit 1c1e34b
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 87 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 14 additions & 11 deletions crates/dap/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,13 @@ impl DebugAdapterClient {
arguments: Some(serialized_arguments),
};

{
self.transport
.current_requests
.lock()
.await
.insert(sequence_id, callback_tx);
}

self.transport
.server_tx
.send(Message::Request(request))
.await?;
.current_requests
.lock()
.await
.insert(sequence_id, callback_tx);

self.respond(Message::Request(request)).await?;

let response = callback_rx.recv().await??;

Expand All @@ -180,6 +175,14 @@ impl DebugAdapterClient {
}
}

pub async fn respond(&self, message: Message) -> Result<()> {
self.transport
.server_tx
.send(message)
.await
.map_err(|e| anyhow::anyhow!("Failed to send response back: {}", e))
}

pub fn id(&self) -> DebugAdapterClientId {
self.id
}
Expand Down
4 changes: 0 additions & 4 deletions crates/dap_adapters/src/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,6 @@ impl DebugAdapter for JsDebugAdapter {
json!({
"program": config.program,
"type": "pwa-node",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
]
})
}
}
2 changes: 2 additions & 0 deletions crates/debugger_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ workspace = true

[dependencies]
anyhow.workspace = true
collections.workspace = true
dap.workspace = true
editor.workspace = true
futures.workspace = true
Expand All @@ -24,6 +25,7 @@ serde_json.workspace = true
settings.workspace = true
task.workspace = true
tasks_ui.workspace = true
terminal_view.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
Expand Down
195 changes: 147 additions & 48 deletions crates/debugger_ui/src/debugger_panel.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
use crate::debugger_panel_item::DebugPanelItem;
use anyhow::Result;
use dap::client::DebugAdapterClient;
use collections::{BTreeMap, HashMap};
use dap::client::{DebugAdapterClientId, ThreadStatus};
use dap::debugger_settings::DebuggerSettings;
use dap::messages::{Events, Message};
use dap::requests::{Request, StartDebugging};
use dap::requests::{Request, RunInTerminal, StartDebugging};
use dap::{
Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent,
OutputEvent, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
OutputEvent, RunInTerminalRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent,
ThreadEventReason,
};
use gpui::{
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
FontWeight, Model, Subscription, Task, View, ViewContext, WeakView,
};
use project::dap_store::DapStore;
use project::terminals::TerminalKind;
use serde_json::Value;
use settings::Settings;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::path::PathBuf;
use std::u64;
use terminal_view::terminal_panel::TerminalPanel;
use ui::prelude::*;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Expand Down Expand Up @@ -92,28 +94,29 @@ impl DebugPanel {
cx.subscribe(&pane, Self::handle_pane_event),
cx.subscribe(&project, {
move |this: &mut Self, _, event, cx| match event {
project::Event::DebugClientEvent { message, client_id } => {
let Some(client) = this.debug_client_by_id(client_id, cx) else {
return cx.emit(DebugPanelEvent::ClientStopped(*client_id));
};

match message {
Message::Event(event) => {
this.handle_debug_client_events(client_id, event, cx);
}
Message::Request(request) => {
if StartDebugging::COMMAND == request.command {
Self::handle_start_debugging_request(
this,
client,
request.arguments.clone(),
cx,
);
}
project::Event::DebugClientEvent { message, client_id } => match message {
Message::Event(event) => {
this.handle_debug_client_events(client_id, event, cx);
}
Message::Request(request) => {
if StartDebugging::COMMAND == request.command {
this.handle_start_debugging_request(
client_id,
request.seq,
request.arguments.clone(),
cx,
);
} else if RunInTerminal::COMMAND == request.command {
this.handle_run_in_terminal_request(
client_id,
request.seq,
request.arguments.clone(),
cx,
);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
},
project::Event::DebugClientStopped(client_id) => {
cx.emit(DebugPanelEvent::ClientStopped(*client_id));

Expand All @@ -131,11 +134,11 @@ impl DebugPanel {
pane,
size: px(300.),
_subscriptions,
dap_store: project.read(cx).dap_store(),
focus_handle: cx.focus_handle(),
show_did_not_stop_warning: false,
thread_states: Default::default(),
workspace: workspace.weak_handle(),
dap_store: project.read(cx).dap_store(),
}
})
}
Expand All @@ -159,23 +162,6 @@ impl DebugPanel {
.and_then(|panel| panel.downcast::<DebugPanelItem>())
}

fn debug_client_by_id(
&self,
client_id: &DebugAdapterClientId,
cx: &mut ViewContext<Self>,
) -> Option<Arc<DebugAdapterClient>> {
self.workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.dap_store()
.read(cx)
.client_by_id(client_id)
})
.ok()
.flatten()
}

fn handle_pane_event(
&mut self,
_: View<Pane>,
Expand Down Expand Up @@ -227,20 +213,133 @@ impl DebugPanel {
}

fn handle_start_debugging_request(
this: &mut Self,
client: Arc<DebugAdapterClient>,
&mut self,
client_id: &DebugAdapterClientId,
seq: u64,
request_args: Option<Value>,
cx: &mut ViewContext<Self>,
) {
let start_args = if let Some(args) = request_args {
let args = if let Some(args) = request_args {
serde_json::from_value(args.clone()).ok()
} else {
None
};

this.dap_store.update(cx, |store, cx| {
store.start_client(client.config(), start_args, cx);
self.dap_store.update(cx, |store, cx| {
store
.respond_to_start_debugging(client_id, seq, args, cx)
.detach_and_log_err(cx);
});
}

fn handle_run_in_terminal_request(
&mut self,
client_id: &DebugAdapterClientId,
seq: u64,
request_args: Option<Value>,
cx: &mut ViewContext<Self>,
) {
let Some(request_args) = request_args else {
self.dap_store.update(cx, |store, cx| {
store
.respond_to_run_in_terminal(client_id, false, seq, None, cx)
.detach_and_log_err(cx);
});

return;
};

let request_args: RunInTerminalRequestArguments =
serde_json::from_value(request_args).unwrap();

let mut envs: HashMap<String, String> = Default::default();

if let Some(Value::Object(env)) = request_args.env {
// Special handling for VSCODE_INSPECTOR_OPTIONS:
// The JavaScript debug adapter expects this value to be a valid JSON object.
// However, it's often passed as an escaped string, which the adapter can't parse.
// We need to unescape it and reformat it so the adapter can read it correctly.
for (key, value) in env {
let value_str = match (key.as_str(), value) {
("VSCODE_INSPECTOR_OPTIONS", Value::String(value)) => {
serde_json::from_str::<Value>(&value[3..])
.map(|json| format!(":::{}", json))
.unwrap_or_else(|_| value)
}
(_, value) => value.to_string(),
};

envs.insert(key, value_str.trim_matches('"').to_string());
}
}

let terminal_task = self.workspace.update(cx, |workspace, cx| {
let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();

terminal_panel.update(cx, |terminal_panel, cx| {
let mut args = request_args.args.clone();

// Handle special case for NodeJS debug adapter
// If only the Node binary path is provided, we set the command to None
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
// This allows the NodeJS debug client to attach correctly
let command = if args.len() > 1 {
Some(args.remove(0))
} else {
None
};

let terminal_task = terminal_panel.add_terminal(
TerminalKind::Debug {
command,
args,
envs,
cwd: PathBuf::from(request_args.cwd),
},
task::RevealStrategy::Always,
cx,
);

cx.spawn(|_, mut cx| async move {
let pid_task = async move {
let terminal = terminal_task.await?;

terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid())
};

pid_task.await
})
})
});

let client_id = *client_id;
cx.spawn(|this, mut cx| async move {
// Ensure a response is always sent, even in error cases,
// to maintain proper communication with the debug adapter
let (success, pid) = match terminal_task {
Ok(pid_task) => match pid_task.await {
Ok(pid) => (true, pid),
Err(_) => (false, None),
},
Err(_) => (false, None),
};

let respond_task = this.update(&mut cx, |this, cx| {
this.dap_store.update(cx, |store, cx| {
store.respond_to_run_in_terminal(
&client_id,
success,
seq,
pid.map(|pid| pid.as_u32() as u64),
cx,
)
})
});

respond_task?.await
})
.detach_and_log_err(cx);
}

fn handle_debug_client_events(
Expand Down
Loading

0 comments on commit 1c1e34b

Please sign in to comment.