Skip to content

Commit ddbf4e3

Browse files
authored
Merge pull request #2083 from harehare/feat/dap-exception-breakpoints
✨ feat(mq-dap): support "Uncaught Exceptions" breakpoint filter
2 parents 6469584 + 120d0b8 commit ddbf4e3

10 files changed

Lines changed: 395 additions & 27 deletions

File tree

crates/mq-dap/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ Once connected to a DAP client:
2424
2. **Conditional Breakpoints**: Only stop when an mq expression evaluates truthy, e.g. `x > 3`
2525
3. **Hit Count Breakpoints**: Only stop once the hit count condition is met. A bare number, e.g. `3`, is shorthand for `hit_count >= 3`; otherwise it's evaluated as an mq expression with `hit_count` bound to the current hit count, e.g. `hit_count >= 3 && x == 1`
2626
4. **Logpoints**: Log a message instead of stopping; uses mq's own `${expr}` string interpolation syntax, e.g. `x is ${x}`. `${self}` yields the current pipeline value and `${$VAR}` reads the environment variable `VAR`
27-
5. **Start Debugging**: Launch the debugger with your query file
28-
6. **Step Through Code**: Use step over, step in, and step out commands
29-
7. **Inspect Variables**: Hover over variables or view them in the variables pane
30-
8. **Watch Expressions**: Add mq expressions to your editor's watch pane to re-evaluate them against the current scope every time execution stops
31-
9. **View Call Stack**: See the current execution stack in the call stack pane
27+
5. **Exception Breakpoints**: Enable "Uncaught Exceptions" in your editor's breakpoints pane to automatically pause when a query raises an error that isn't caught by `try`/`catch`
28+
6. **Start Debugging**: Launch the debugger with your query file
29+
7. **Step Through Code**: Use step over, step in, and step out commands
30+
8. **Inspect Variables**: Hover over variables or view them in the variables pane
31+
9. **Watch Expressions**: Add mq expressions to your editor's watch pane to re-evaluate them against the current scope every time execution stops
32+
10. **View Call Stack**: See the current execution stack in the call stack pane
3233

3334
### Example Debug Session
3435

crates/mq-dap/src/adapter.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use mq_lang::Shared;
99
use std::borrow::Cow;
1010
use std::io;
1111
use std::path::PathBuf;
12+
use std::sync::Arc;
13+
use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
1214
use std::thread;
1315
use tracing::{debug, error};
1416

@@ -19,6 +21,11 @@ use crate::protocol::{DapCommand, DebuggerMessage, LaunchArgs};
1921

2022
type DynResult<T> = miette::Result<T, Box<dyn std::error::Error>>;
2123

24+
/// The `filter` ID for the "Uncaught Exceptions" exception breakpoint filter advertised in
25+
/// `Initialize`'s `exceptionBreakpointFilters` capability (see [`crate::server::start`]) and
26+
/// checked in [`MqAdapter::handle_request`]'s handling of `setExceptionBreakpoints`.
27+
pub const UNCAUGHT_EXCEPTIONS_FILTER: &str = "uncaught";
28+
2229
/// Main DAP adapter for mq debugger
2330
pub struct MqAdapter {
2431
engine: mq_lang::DefaultEngine,
@@ -27,6 +34,7 @@ pub struct MqAdapter {
2734
debugger_message_tx: Option<Sender<DebuggerMessage>>,
2835
dap_command_tx: Option<Sender<DapCommand>>,
2936
current_debug_context: Option<mq_lang::DebugContext>,
37+
exception_breakpoints_enabled: Arc<AtomicBool>,
3038
}
3139

3240
impl Default for MqAdapter {
@@ -40,13 +48,18 @@ impl MqAdapter {
4048
// Create channels for communication between DAP server and debugger handler
4149
let (message_tx, message_rx) = crossbeam_channel::unbounded::<DebuggerMessage>();
4250
let (command_tx, command_rx) = crossbeam_channel::unbounded::<DapCommand>();
51+
let exception_breakpoints_enabled = Arc::new(AtomicBool::new(false));
4352

4453
let dap_handler = DapDebuggerHandler::new(message_tx.clone());
4554
let mut engine = mq_lang::DefaultEngine::default();
4655

4756
// Set up the debugger handler
4857
{
49-
let handler_boxed = Box::new(DapHandlerWrapper::new(dap_handler, command_rx));
58+
let handler_boxed = Box::new(DapHandlerWrapper::new(
59+
dap_handler,
60+
command_rx,
61+
Arc::clone(&exception_breakpoints_enabled),
62+
));
5063
engine.set_debugger_handler(handler_boxed);
5164
}
5265

@@ -57,6 +70,7 @@ impl MqAdapter {
5770
dap_command_tx: Some(command_tx),
5871
query_file: None,
5972
current_debug_context: None,
73+
exception_breakpoints_enabled,
6074
}
6175
}
6276

@@ -209,6 +223,26 @@ impl MqAdapter {
209223
debug!(message = %message, "Sending output event for logpoint");
210224
self.send_log_output(&message, server)?;
211225
}
226+
DebuggerMessage::ExceptionPaused {
227+
thread_id,
228+
message,
229+
context,
230+
} => {
231+
// Store the current debug context for variable inspection
232+
self.current_debug_context = Some(context);
233+
debug!(message = %message, "Sending stopped event for exception");
234+
235+
let event = Event::Stopped(events::StoppedEventBody {
236+
reason: types::StoppedEventReason::Exception,
237+
description: Some("Paused on error".to_string()),
238+
thread_id: Some(thread_id),
239+
preserve_focus_hint: None,
240+
text: Some(message),
241+
all_threads_stopped: None,
242+
hit_breakpoint_ids: None,
243+
});
244+
server.send_event(event)?;
245+
}
212246
DebuggerMessage::Terminated => {
213247
debug!("Sending terminated event");
214248

@@ -320,8 +354,12 @@ impl MqAdapter {
320354
let rsp = req.success(ResponseBody::Launch);
321355
server.respond(rsp)?;
322356
}
323-
Command::SetExceptionBreakpoints(_) => {
324-
debug!("Received SetExceptionBreakpoints request");
357+
Command::SetExceptionBreakpoints(args) => {
358+
debug!(?args, "Received SetExceptionBreakpoints request");
359+
360+
let enabled = args.filters.iter().any(|filter| filter == UNCAUGHT_EXCEPTIONS_FILTER);
361+
self.exception_breakpoints_enabled.store(enabled, SeqCst);
362+
325363
let rsp = req.success(ResponseBody::SetExceptionBreakpoints(SetExceptionBreakpointsResponse {
326364
breakpoints: None,
327365
}));
@@ -772,6 +810,24 @@ mod tests {
772810
assert!(adapter.current_debug_context.is_none());
773811
}
774812

813+
#[test]
814+
fn test_handle_debugger_message_exception_paused() {
815+
let mut adapter = MqAdapter::new();
816+
let input = BufReader::new(Cursor::new(Vec::new()));
817+
let output = BufWriter::new(Cursor::new(Vec::new()));
818+
let mut server = Server::new(input, output);
819+
820+
let message = DebuggerMessage::ExceptionPaused {
821+
thread_id: 1,
822+
message: "boom".to_string(),
823+
context: mq_lang::DebugContext::default(),
824+
};
825+
826+
let result = adapter.handle_debugger_message(message, &mut server);
827+
assert!(result.is_ok());
828+
assert!(adapter.current_debug_context.is_some());
829+
}
830+
775831
#[test]
776832
fn test_handle_debugger_message_step_completed() {
777833
let mut adapter = MqAdapter::new();
@@ -1128,6 +1184,49 @@ mod tests {
11281184
assert_eq!(stored[0].log_message.as_deref(), Some("x is {x}"));
11291185
}
11301186

1187+
#[test]
1188+
fn test_handle_request_set_exception_breakpoints_enables_uncaught_filter() {
1189+
let mut adapter = MqAdapter::new();
1190+
let input = BufReader::new(Cursor::new(Vec::new()));
1191+
let output = BufWriter::new(Cursor::new(Vec::new()));
1192+
let mut server = Server::new(input, output);
1193+
1194+
let req = Request {
1195+
seq: 1,
1196+
command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments {
1197+
filters: vec![UNCAUGHT_EXCEPTIONS_FILTER.to_string()],
1198+
filter_options: None,
1199+
exception_options: None,
1200+
}),
1201+
};
1202+
1203+
let result = adapter.handle_request(req, &mut server);
1204+
assert!(result.is_ok());
1205+
assert!(adapter.exception_breakpoints_enabled.load(SeqCst));
1206+
}
1207+
1208+
#[test]
1209+
fn test_handle_request_set_exception_breakpoints_disables_when_filter_absent() {
1210+
let mut adapter = MqAdapter::new();
1211+
adapter.exception_breakpoints_enabled.store(true, SeqCst);
1212+
let input = BufReader::new(Cursor::new(Vec::new()));
1213+
let output = BufWriter::new(Cursor::new(Vec::new()));
1214+
let mut server = Server::new(input, output);
1215+
1216+
let req = Request {
1217+
seq: 1,
1218+
command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments {
1219+
filters: vec![],
1220+
filter_options: None,
1221+
exception_options: None,
1222+
}),
1223+
};
1224+
1225+
let result = adapter.handle_request(req, &mut server);
1226+
assert!(result.is_ok());
1227+
assert!(!adapter.exception_breakpoints_enabled.load(SeqCst));
1228+
}
1229+
11311230
#[test]
11321231
fn test_handle_request_set_expression() {
11331232
let mut adapter = MqAdapter::new();

0 commit comments

Comments
 (0)