@@ -0,0 +1,311 @@
1+use crate::editor::{Action, Breakpoint};
2+use crate::{align_view, Align, Editor};
3+use helix_core::Selection;
4+use helix_dap::{self as dap, Client, Payload, Request, ThreadId};
5+use helix_lsp::block_on;
7+use std::path::PathBuf;
10+macro_rules! debugger {
12+ match &mut $editor.debugger {
13+ Some(debugger) => debugger,
20+pub fn dap_pos_to_pos(doc: &helix_core::Rope, line: usize, column: usize) -> Option<usize> {
21+ // 1-indexing to 0 indexing
22+ let line = doc.try_line_to_char(line - 1).ok()?;
23+ let pos = line + column.saturating_sub(1);
24+ // TODO: this is probably utf-16 offsets
28+pub async fn select_thread_id(editor: &mut Editor, thread_id: ThreadId, force: bool) {
29+ let debugger = debugger!(editor);
31+ if !force && debugger.thread_id.is_some() {
35+ debugger.thread_id = Some(thread_id);
36+ fetch_stack_trace(debugger, thread_id).await;
38+ let frame = debugger.stack_frames[&thread_id].get(0).cloned();
39+ if let Some(frame) = &frame {
40+ jump_to_stack_frame(editor, frame);
44+pub async fn fetch_stack_trace(debugger: &mut Client, thread_id: ThreadId) {
45+ let (frames, _) = match debugger.stack_trace(thread_id).await {
46+ Ok(frames) => frames,
49+ debugger.stack_frames.insert(thread_id, frames);
50+ debugger.active_frame = Some(0);
53+pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) {
54+ let path = if let Some(helix_dap::Source {
55+ path: Some(ref path),
64+ if let Err(e) = editor.open(path, Action::Replace) {
65+ editor.set_error(format!("Unable to jump to stack frame: {}", e));
69+ let (view, doc) = current!(editor);
71+ let text_end = doc.text().len_chars().saturating_sub(1);
72+ let start = dap_pos_to_pos(doc.text(), frame.line, frame.column).unwrap_or(0);
75+ .and_then(|end_line| dap_pos_to_pos(doc.text(), end_line, frame.end_column.unwrap_or(0)))
78+ let selection = Selection::single(start.min(text_end), end.min(text_end));
79+ doc.set_selection(view.id, selection);
80+ align_view(doc, view, Align::Center);
83+pub fn breakpoints_changed(
84+ debugger: &mut dap::Client,
86+ breakpoints: &mut [Breakpoint],
87+) -> Result<(), anyhow::Error> {
88+ // TODO: handle capabilities correctly again, by filterin breakpoints when emitting
89+ // if breakpoint.condition.is_some()
94+ // .supports_conditional_breakpoints
95+ // .unwrap_or_default()
98+ // "Can't edit breakpoint: debugger does not support conditional breakpoints"
101+ // if breakpoint.log_message.is_some()
106+ // .supports_log_points
107+ // .unwrap_or_default()
109+ // bail!("Can't edit breakpoint: debugger does not support logpoints")
111+ let source_breakpoints = breakpoints
113+ .map(|breakpoint| helix_dap::SourceBreakpoint {
114+ line: breakpoint.line + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
115+ ..Default::default()
117+ .collect::<Vec<_>>();
119+ let request = debugger.set_breakpoints(path, source_breakpoints);
120+ match block_on(request) {
121+ Ok(Some(dap_breakpoints)) => {
122+ for (breakpoint, dap_breakpoint) in breakpoints.iter_mut().zip(dap_breakpoints) {
123+ breakpoint.id = dap_breakpoint.id;
124+ breakpoint.verified = dap_breakpoint.verified;
125+ breakpoint.message = dap_breakpoint.message;
126+ // TODO: handle breakpoint.message
127+ // TODO: verify source matches
128+ breakpoint.line = dap_breakpoint.line.unwrap_or(0).saturating_sub(1); // convert to 0-indexing
130+ breakpoint.column = dap_breakpoint.column;
131+ // TODO: verify end_linef/col instruction reference, offset
134+ Err(e) => anyhow::bail!("Failed to set breakpoints: {}", e),
141+ pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) -> bool {
142+ use dap::requests::RunInTerminal;
143+ use helix_dap::{events, Event};
145+ let debugger = match self.debugger.as_mut() {
146+ Some(debugger) => debugger,
147+ None => return false,
150+ Payload::Event(ev) => match *ev {
151+ Event::Stopped(events::Stopped {
156+ all_threads_stopped,
159+ let all_threads_stopped = all_threads_stopped.unwrap_or_default();
161+ if all_threads_stopped {
162+ if let Ok(response) = debugger.request::<dap::requests::Threads>(()).await {
163+ for thread in response.threads {
164+ fetch_stack_trace(debugger, thread.id).await;
166+ select_thread_id(self, thread_id.unwrap_or_default(), false).await;
168+ } else if let Some(thread_id) = thread_id {
169+ debugger.thread_states.insert(thread_id, reason.clone()); // TODO: dap uses "type" || "reason" here
171+ // whichever thread stops is made "current" (if no previously selected thread).
172+ select_thread_id(self, thread_id, false).await;
175+ let scope = match thread_id {
176+ Some(id) => format!("Thread {}", id),
177+ None => "Target".to_owned(),
180+ let mut status = format!("{} stopped because of {}", scope, reason);
181+ if let Some(desc) = description {
182+ status.push_str(&format!(" {}", desc));
184+ if let Some(text) = text {
185+ status.push_str(&format!(" {}", text));
187+ if all_threads_stopped {
188+ status.push_str(" (all threads stopped)");
191+ self.set_status(status);
193+ Event::Continued(events::Continued { thread_id, .. }) => {
196+ .insert(thread_id, "running".to_owned());
197+ if debugger.thread_id == Some(thread_id) {
198+ debugger.resume_application();
201+ Event::Thread(_) => {
202+ // TODO: update thread_states, make threads request
204+ Event::Breakpoint(events::Breakpoint { reason, breakpoint }) => {
207+ if let Some(source) = breakpoint.source {
209+ .entry(source.path.unwrap()) // TODO: no unwraps
213+ verified: breakpoint.verified,
214+ message: breakpoint.message,
215+ line: breakpoint.line.unwrap().saturating_sub(1), // TODO: no unwrap
216+ column: breakpoint.column,
217+ ..Default::default()
222+ for breakpoints in self.breakpoints.values_mut() {
224+ breakpoints.iter().position(|b| b.id == breakpoint.id)
226+ breakpoints[i].verified = breakpoint.verified;
227+ breakpoints[i].message = breakpoint.message.clone();
228+ breakpoints[i].line =
229+ breakpoint.line.unwrap().saturating_sub(1); // TODO: no unwrap
230+ breakpoints[i].column = breakpoint.column;
235+ for breakpoints in self.breakpoints.values_mut() {
237+ breakpoints.iter().position(|b| b.id == breakpoint.id)
239+ breakpoints.remove(i);
244+ warn!("Unknown breakpoint event: {}", reason);
248+ Event::Output(events::Output {
249+ category, output, ..
251+ let prefix = match category {
253+ if &category == "telemetry" {
256+ format!("Debug ({}):", category)
258+ None => "Debug:".to_owned(),
261+ log::info!("{}", output);
262+ self.set_status(format!("{} {}", prefix, output));
264+ Event::Initialized => {
265+ // send existing breakpoints
266+ for (path, breakpoints) in &mut self.breakpoints {
267+ // TODO: call futures in parallel, await all
268+ let _ = breakpoints_changed(debugger, path.clone(), breakpoints);
270+ // TODO: fetch breakpoints (in case we're attaching)
272+ if debugger.configuration_done().await.is_ok() {
273+ self.set_status("Debugged application started");
274+ }; // TODO: do we need to handle error?
277+ log::warn!("Unhandled event {:?}", ev);
278+ return false; // return early to skip render
281+ Payload::Response(_) => unreachable!(),
282+ Payload::Request(request) => match request.command.as_str() {
283+ RunInTerminal::COMMAND => {
284+ let arguments: dap::requests::RunInTerminalArguments =
285+ serde_json::from_value(request.arguments.unwrap_or_default()).unwrap();
288+ let process = std::process::Command::new("tmux")
289+ .arg("split-window")
290+ .arg(arguments.args.join(" "))
297+ dap::requests::RunInTerminal::COMMAND,
298+ serde_json::to_value(dap::requests::RunInTerminalResponse {
299+ process_id: Some(process.id()),
300+ shell_process_id: None,
302+ .map_err(|e| e.into()),
306+ _ => log::error!("DAP reverse request not implemented: {:?}", request),