Skip to main content

perl_dap/debug_adapter/
dispatch.rs

1//! Request dispatching: handle_request, dispatch_request, response_succeeded_for_command.
2
3use super::*;
4
5impl DebugAdapter {
6    const SUPPORTED_COMMANDS: [&str; 37] = [
7        "initialize",
8        "launch",
9        "attach",
10        "disconnect",
11        "terminate",
12        "setBreakpoints",
13        "setFunctionBreakpoints",
14        "setExceptionBreakpoints",
15        "configurationDone",
16        "threads",
17        "stackTrace",
18        "scopes",
19        "variables",
20        "setVariable",
21        "continue",
22        "next",
23        "stepIn",
24        "stepOut",
25        "pause",
26        "evaluate",
27        "inlineValues",
28        "breakpointLocations",
29        "source",
30        "loadedSources",
31        "modules",
32        "completions",
33        "exceptionInfo",
34        "restart",
35        "setExpression",
36        "dataBreakpointInfo",
37        "setDataBreakpoints",
38        "cancel",
39        "stepInTargets",
40        "gotoTargets",
41        "goto",
42        "restartFrame",
43        "terminateThreads",
44    ];
45
46    /// Dispatch a DAP request and return the response message.
47    ///
48    /// Emits the `initialized` event automatically when an `initialize` request
49    /// succeeds. This mirrors the behavior expected by DAP-compliant clients.
50    pub fn handle_request(
51        &mut self,
52        request_seq: i64,
53        command: &str,
54        arguments: Option<Value>,
55    ) -> DapMessage {
56        tracing::debug!(command, arguments = ?arguments, "DAP request");
57
58        let response = self.dispatch_request(request_seq, command, arguments);
59
60        // Preserve existing direct-call behavior for tests and in-memory usage.
61        if command == "initialize" && Self::response_succeeded_for_command(&response, "initialize")
62        {
63            self.send_event("initialized", None);
64        }
65
66        response
67    }
68
69    /// Handle a DAP request (mock version for testing)
70    pub fn handle_request_mock(
71        &mut self,
72        request_seq: i64,
73        command: &str,
74        arguments: Option<Value>,
75    ) -> DapMessage {
76        tracing::debug!(command, arguments = ?arguments, "DAP request (mock)");
77
78        let response = self.dispatch_request(request_seq, command, arguments);
79        if command == "initialize" && Self::response_succeeded_for_command(&response, "initialize")
80        {
81            self.send_event("initialized", None);
82        }
83        response
84    }
85
86    pub(super) fn dispatch_request(
87        &mut self,
88        request_seq: i64,
89        command: &str,
90        arguments: Option<Value>,
91    ) -> DapMessage {
92        let seq = self.next_seq();
93
94        match command {
95            "initialize" => self.handle_initialize(seq, request_seq, arguments),
96            "launch" => self.handle_launch(seq, request_seq, arguments),
97            "attach" => self.handle_attach(seq, request_seq, arguments),
98            "disconnect" => self.handle_disconnect(seq, request_seq, arguments),
99            "terminate" => self.handle_terminate(seq, request_seq, arguments),
100            "setBreakpoints" => self.handle_set_breakpoints(seq, request_seq, arguments),
101            "setFunctionBreakpoints" => {
102                self.handle_set_function_breakpoints(seq, request_seq, arguments)
103            }
104            "setExceptionBreakpoints" => {
105                self.handle_set_exception_breakpoints(seq, request_seq, arguments)
106            }
107            "configurationDone" => self.handle_configuration_done(seq, request_seq),
108            "threads" => self.handle_threads(seq, request_seq),
109            "stackTrace" => self.handle_stack_trace(seq, request_seq, arguments),
110            "scopes" => self.handle_scopes(seq, request_seq, arguments),
111            "variables" => self.handle_variables(seq, request_seq, arguments),
112            "setVariable" => self.handle_set_variable(seq, request_seq, arguments),
113            "continue" => self.handle_continue(seq, request_seq, arguments),
114            "next" => self.handle_next(seq, request_seq, arguments),
115            "stepIn" => self.handle_step_in(seq, request_seq, arguments),
116            "stepOut" => self.handle_step_out(seq, request_seq, arguments),
117            "pause" => self.handle_pause(seq, request_seq, arguments),
118            "evaluate" => self.handle_evaluate(seq, request_seq, arguments),
119            "inlineValues" => self.handle_inline_values(seq, request_seq, arguments),
120            "breakpointLocations" => self.handle_breakpoint_locations(seq, request_seq, arguments),
121            "source" => self.handle_source(seq, request_seq, arguments),
122            "loadedSources" => self.handle_loaded_sources(seq, request_seq, arguments),
123            "modules" => self.handle_modules(seq, request_seq, arguments),
124            "completions" => self.handle_completions(seq, request_seq, arguments),
125            "exceptionInfo" => self.handle_exception_info(seq, request_seq, arguments),
126            "restart" => self.handle_restart(seq, request_seq, arguments),
127            "setExpression" => self.handle_set_expression(seq, request_seq, arguments),
128            "dataBreakpointInfo" => self.handle_data_breakpoint_info(seq, request_seq, arguments),
129            "setDataBreakpoints" => self.handle_set_data_breakpoints(seq, request_seq, arguments),
130            "cancel" => self.handle_cancel(seq, request_seq, arguments),
131            "stepInTargets" => self.handle_step_in_targets(seq, request_seq, arguments),
132            "gotoTargets" => self.handle_goto_targets(seq, request_seq, arguments),
133            "goto" => self.handle_goto(seq, request_seq, arguments),
134            "restartFrame" => self.handle_restart_frame(seq, request_seq, arguments),
135            "terminateThreads" => self.handle_terminate_threads(seq, request_seq, arguments),
136            _ => DapMessage::Response {
137                seq,
138                request_seq,
139                success: false,
140                command: command.to_string(),
141                body: None,
142                message: Some(Self::unknown_command_message(command)),
143            },
144        }
145    }
146
147    fn unknown_command_message(command: &str) -> String {
148        if let Some(suggestion) = Self::suggested_command(command) {
149            format!("Unknown command: {command}. Did you mean '{suggestion}'?")
150        } else {
151            format!("Unknown command: {command}")
152        }
153    }
154
155    fn suggested_command(command: &str) -> Option<&'static str> {
156        if let Some(case_suggestion) = Self::SUPPORTED_COMMANDS
157            .iter()
158            .copied()
159            .find(|known| known.eq_ignore_ascii_case(command))
160        {
161            return Some(case_suggestion);
162        }
163
164        Self::SUPPORTED_COMMANDS
165            .iter()
166            .copied()
167            .filter_map(|known| {
168                let distance = Self::command_edit_distance(command, known);
169                (distance <= Self::suggestion_threshold(command, known))
170                    .then_some((known, distance))
171            })
172            .min_by_key(|(known, distance)| (*distance, known.len()))
173            .map(|(known, _)| known)
174    }
175
176    fn suggestion_threshold(command: &str, known: &str) -> usize {
177        command.len().max(known.len()).saturating_div(4).clamp(1, 4)
178    }
179
180    fn command_edit_distance(left: &str, right: &str) -> usize {
181        let left = left.to_ascii_lowercase();
182        let right = right.to_ascii_lowercase();
183        let left_bytes = left.as_bytes();
184        let right_bytes = right.as_bytes();
185
186        let mut previous: Vec<usize> = (0..=right_bytes.len()).collect();
187        let mut current = vec![0; right_bytes.len() + 1];
188
189        for (left_index, left_byte) in left_bytes.iter().enumerate() {
190            current[0] = left_index + 1;
191
192            for (right_index, right_byte) in right_bytes.iter().enumerate() {
193                let substitution_cost = usize::from(left_byte != right_byte);
194                let deletion = previous[right_index + 1] + 1;
195                let insertion = current[right_index] + 1;
196                let substitution = previous[right_index] + substitution_cost;
197                current[right_index + 1] = deletion.min(insertion).min(substitution);
198            }
199
200            std::mem::swap(&mut previous, &mut current);
201        }
202
203        previous[right_bytes.len()]
204    }
205
206    pub(super) fn response_succeeded_for_command(
207        response: &DapMessage,
208        expected_command: &str,
209    ) -> bool {
210        matches!(
211            response,
212            DapMessage::Response {
213                success: true,
214                command,
215                ..
216            } if command == expected_command
217        )
218    }
219}