1use std::collections::HashMap;
18use std::io::{BufRead, BufReader, Read, Write};
19use std::net::TcpStream;
20use std::path::{Path, PathBuf};
21use std::process::{Child, Command, Stdio};
22use std::sync::mpsc::{self, Receiver, TryRecvError};
23use std::thread;
24use std::time::{Duration, Instant};
25
26use serde_json::{json, Value};
27
28pub const DAP_PANEL_ANIM_MS: u64 = 200;
30const CONFIG_FALLBACK: Duration = Duration::from_secs(2);
32const SHUTDOWN_GRACE: Duration = Duration::from_millis(1000);
34const STACK_LEVELS: u64 = 40;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DapState {
41 Idle,
42 Starting,
43 Running,
44 Stopped,
45 Ending,
46}
47
48impl DapState {
49 pub fn label(self) -> &'static str {
50 match self {
51 DapState::Idle => "idle",
52 DapState::Starting => "starting",
53 DapState::Running => "running",
54 DapState::Stopped => "stopped",
55 DapState::Ending => "ending",
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct Breakpoint {
62 pub line: usize,
64 pub verified: bool,
65 pub message: String,
66 pub condition: Option<String>,
68 pub log_message: Option<String>,
70}
71
72#[derive(Debug, Clone)]
73pub struct StackFrameInfo {
74 pub id: i64,
75 pub name: String,
76 pub path: String,
77 pub line: usize,
79 pub column: usize,
80}
81
82#[derive(Debug, Clone)]
84pub struct VarNode {
85 pub name: String,
86 pub value: String,
87 pub typ: String,
88 pub var_ref: i64,
90 pub depth: usize,
91 pub expanded: bool,
92 pub is_scope: bool,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum DebugPane {
97 Stack,
98 Variables,
99 Breakpoints,
100 Console,
101}
102
103impl DebugPane {
104 pub fn label(self) -> &'static str {
105 match self {
106 DebugPane::Stack => "Stack",
107 DebugPane::Variables => "Vars",
108 DebugPane::Breakpoints => "BPs",
109 DebugPane::Console => "Console",
110 }
111 }
112
113 pub fn next(self) -> Self {
114 match self {
115 DebugPane::Stack => DebugPane::Variables,
116 DebugPane::Variables => DebugPane::Breakpoints,
117 DebugPane::Breakpoints => DebugPane::Console,
118 DebugPane::Console => DebugPane::Stack,
119 }
120 }
121
122 pub fn prev(self) -> Self {
123 match self {
124 DebugPane::Stack => DebugPane::Console,
125 DebugPane::Variables => DebugPane::Stack,
126 DebugPane::Breakpoints => DebugPane::Variables,
127 DebugPane::Console => DebugPane::Breakpoints,
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
133enum PendingKind {
134 Initialize,
135 Launch,
137 SetBreakpoints(String),
138 ExceptionBreakpoints,
139 ConfigDone,
140 StackTrace,
141 Scopes,
142 Variables(i64),
144 Threads,
145 Continue,
146 Next,
147 StepIn,
148 StepOut,
149 Pause,
150 Terminate,
151 Disconnect,
152 Evaluate,
153}
154
155pub struct DapClient {
158 writer: Option<Box<dyn Write + Send>>,
160 rx: Option<Receiver<Value>>,
161 child: Option<Child>,
162 next_id: u64,
163 pending: HashMap<u64, PendingKind>,
164
165 pub state: DapState,
166 pub adapter_name: String,
167 pub error: Option<String>,
168 pub soft_error: Option<String>,
170
171 pub breakpoints: HashMap<String, Vec<Breakpoint>>,
173 pub stack: Vec<StackFrameInfo>,
174 pub vars: Vec<VarNode>,
176 pub console: Vec<String>,
177 pub threads: Vec<(i64, String)>,
179
180 pub selected_frame: usize,
181 pub selected_bp: usize,
182 pub pane: DebugPane,
183 pub focus_row: usize,
184
185 pub thread_id: Option<i64>,
186 pub stopped_reason: Option<String>,
187 pub current_path: Option<String>,
189 pub current_line: Option<usize>,
190
191 pub panel_open: bool,
193 pub location_dirty: bool,
195 pub last_program: Option<String>,
197 pub last_cwd: Option<String>,
198 pub last_lang: Option<String>,
199 pub last_args: Vec<String>,
200 pub last_attach: Option<String>,
202
203 supports_config_done: bool,
205 supports_terminate: bool,
206 exception_filters: Vec<String>,
208 launch_sent_at: Option<Instant>,
210 config_sent: bool,
212 launch_body: Option<Value>,
214 is_attach: bool,
216 shutdown_deadline: Option<Instant>,
218 restart_pending: Option<(String, Option<PathBuf>, Option<String>, Vec<String>)>,
220 pause_requested: bool,
222 awaiting_stack_thread: bool,
224
225 children_cache: HashMap<i64, Vec<VarNode>>,
227 canon_cache: HashMap<String, String>,
229
230 opened_at: Option<Instant>,
232 anim_pending: bool,
233
234 pub eval_input: String,
236 build_rx: Option<Receiver<Result<(String, PathBuf, String, Vec<String>), String>>>,
238 pub build_message: Option<String>,
240
241 #[cfg(test)]
243 pub(crate) sent: Vec<Value>,
244}
245
246impl Default for DapClient {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl DapClient {
253 pub fn new() -> Self {
254 Self {
255 writer: None,
256 rx: None,
257 child: None,
258 next_id: 1,
259 pending: HashMap::new(),
260 state: DapState::Idle,
261 adapter_name: String::new(),
262 error: None,
263 soft_error: None,
264 breakpoints: HashMap::new(),
265 stack: Vec::new(),
266 vars: Vec::new(),
267 console: Vec::new(),
268 threads: Vec::new(),
269 selected_frame: 0,
270 selected_bp: 0,
271 pane: DebugPane::Stack,
272 focus_row: 0,
273 thread_id: None,
274 stopped_reason: None,
275 current_path: None,
276 current_line: None,
277 panel_open: false,
278 location_dirty: false,
279 last_program: None,
280 last_cwd: None,
281 last_lang: None,
282 last_args: Vec::new(),
283 last_attach: None,
284 supports_config_done: true,
285 supports_terminate: false,
286 exception_filters: Vec::new(),
287 launch_sent_at: None,
288 config_sent: false,
289 launch_body: None,
290 is_attach: false,
291 shutdown_deadline: None,
292 restart_pending: None,
293 pause_requested: false,
294 awaiting_stack_thread: false,
295 children_cache: HashMap::new(),
296 canon_cache: HashMap::new(),
297 opened_at: None,
298 anim_pending: false,
299 eval_input: String::new(),
300 build_rx: None,
301 build_message: None,
302 #[cfg(test)]
303 sent: Vec::new(),
304 }
305 }
306
307 pub fn is_session(&self) -> bool {
308 matches!(
309 self.state,
310 DapState::Starting | DapState::Running | DapState::Stopped
311 )
312 }
313
314 pub fn arm_panel_animation(&mut self) {
318 self.anim_pending = true;
319 self.opened_at = None;
320 }
321
322 pub fn anim_progress(&mut self) -> f32 {
323 if self.anim_pending {
324 self.anim_pending = false;
325 self.opened_at = Some(Instant::now());
326 return 0.0;
327 }
328 let Some(t0) = self.opened_at else {
329 return 1.0;
330 };
331 (t0.elapsed().as_millis() as f32 / DAP_PANEL_ANIM_MS as f32).min(1.0)
332 }
333
334 pub fn log(&mut self, msg: impl Into<String>) {
337 let was_tail =
338 self.pane == DebugPane::Console && self.focus_row + 1 >= self.console.len();
339 self.console.push(msg.into());
340 if self.console.len() > 400 {
341 let drop_n = self.console.len() - 300;
342 self.console.drain(0..drop_n);
343 self.focus_row = self.focus_row.saturating_sub(drop_n);
344 }
345 if was_tail && self.pane == DebugPane::Console {
346 self.focus_row = self.console.len().saturating_sub(1);
347 }
348 }
349
350 fn canon(&mut self, path: &str) -> String {
354 if let Some(c) = self.canon_cache.get(path) {
355 return c.clone();
356 }
357 let c = std::fs::canonicalize(path)
358 .map(|p| p.display().to_string())
359 .unwrap_or_else(|_| path.to_string());
360 self.canon_cache.insert(path.to_string(), c.clone());
361 c
362 }
363
364 pub fn toggle_breakpoint(&mut self, path: &str, line: usize) -> bool {
366 let path = self.canon(path);
367 let entry = self.breakpoints.entry(path.clone()).or_default();
368 if let Some(i) = entry.iter().position(|b| b.line == line) {
369 entry.remove(i);
370 if entry.is_empty() {
371 self.breakpoints.remove(&path);
372 }
373 if self.is_session() {
374 self.send_set_breakpoints(&path);
375 }
376 let _ = self.persist_breakpoints();
377 return false;
378 }
379 entry.push(Breakpoint {
380 line,
381 verified: false,
382 message: String::new(),
383 condition: None,
384 log_message: None,
385 });
386 entry.sort_by_key(|b| b.line);
387 if self.is_session() {
388 self.send_set_breakpoints(&path);
389 }
390 let _ = self.persist_breakpoints();
391 true
392 }
393
394 pub fn set_breakpoint_condition(
396 &mut self,
397 path: &str,
398 line: usize,
399 condition: Option<String>,
400 ) {
401 let path = self.canon(path);
402 let entry = self.breakpoints.entry(path.clone()).or_default();
403 if let Some(b) = entry.iter_mut().find(|b| b.line == line) {
404 b.condition = condition.filter(|s| !s.trim().is_empty());
405 } else {
406 entry.push(Breakpoint {
407 line,
408 verified: false,
409 message: String::new(),
410 condition: condition.filter(|s| !s.trim().is_empty()),
411 log_message: None,
412 });
413 entry.sort_by_key(|b| b.line);
414 }
415 if self.is_session() {
416 self.send_set_breakpoints(&path);
417 }
418 let _ = self.persist_breakpoints();
419 }
420
421 pub fn set_breakpoint_log(
423 &mut self,
424 path: &str,
425 line: usize,
426 log_message: Option<String>,
427 ) {
428 let path = self.canon(path);
429 let entry = self.breakpoints.entry(path.clone()).or_default();
430 if let Some(b) = entry.iter_mut().find(|b| b.line == line) {
431 b.log_message = log_message.filter(|s| !s.trim().is_empty());
432 } else {
433 entry.push(Breakpoint {
434 line,
435 verified: false,
436 message: String::new(),
437 condition: None,
438 log_message: log_message.filter(|s| !s.trim().is_empty()),
439 });
440 entry.sort_by_key(|b| b.line);
441 }
442 if self.is_session() {
443 self.send_set_breakpoints(&path);
444 }
445 let _ = self.persist_breakpoints();
446 }
447
448 pub fn has_breakpoint(&mut self, path: &str, line: usize) -> bool {
449 let path = self.canon(path);
450 self.breakpoints
451 .get(&path)
452 .map(|v| v.iter().any(|b| b.line == line))
453 .unwrap_or(false)
454 }
455
456 pub fn clear_breakpoints(&mut self) {
457 self.breakpoints.clear();
458 let _ = self.persist_breakpoints();
459 }
460
461 fn breakpoints_path() -> PathBuf {
462 let home = std::env::var("HOME")
463 .or_else(|_| std::env::var("USERPROFILE"))
464 .unwrap_or_else(|_| ".".into());
465 PathBuf::from(home).join(".xei").join("breakpoints")
466 }
467
468 pub fn persist_breakpoints(&self) -> Result<(), String> {
470 let path = Self::breakpoints_path();
471 if let Some(parent) = path.parent() {
472 let _ = std::fs::create_dir_all(parent);
473 }
474 let mut out = String::from("# xei breakpoints — path|line|condition|log\n");
475 let mut keys: Vec<_> = self.breakpoints.keys().cloned().collect();
476 keys.sort();
477 for k in keys {
478 if let Some(list) = self.breakpoints.get(&k) {
479 for b in list {
480 let cond = b.condition.as_deref().unwrap_or("");
481 let log = b.log_message.as_deref().unwrap_or("");
482 out.push_str(&format!("{}|{}|{}|{}\n", k, b.line, cond, log));
483 }
484 }
485 }
486 std::fs::write(path, out).map_err(|e| e.to_string())
487 }
488
489 pub fn load_persisted_breakpoints(&mut self) {
491 let Ok(text) = std::fs::read_to_string(Self::breakpoints_path()) else {
492 return;
493 };
494 for line in text.lines() {
495 let line = line.trim();
496 if line.is_empty() || line.starts_with('#') {
497 continue;
498 }
499 let parts: Vec<&str> = line.splitn(4, '|').collect();
500 if parts.len() < 2 {
501 continue;
502 }
503 let path = parts[0].to_string();
504 let Ok(ln) = parts[1].parse::<usize>() else {
505 continue;
506 };
507 let cond = parts
508 .get(2)
509 .map(|s| s.to_string())
510 .filter(|s| !s.is_empty());
511 let log = parts
512 .get(3)
513 .map(|s| s.to_string())
514 .filter(|s| !s.is_empty());
515 let entry = self.breakpoints.entry(path).or_default();
516 if !entry.iter().any(|b| b.line == ln) {
517 entry.push(Breakpoint {
518 line: ln,
519 verified: false,
520 message: String::new(),
521 condition: cond,
522 log_message: log,
523 });
524 entry.sort_by_key(|b| b.line);
525 }
526 }
527 }
528
529 pub fn lines_for(&mut self, path: &str) -> Vec<usize> {
531 let path = self.canon(path);
532 self.breakpoints
533 .get(&path)
534 .map(|v| v.iter().map(|b| b.line).collect())
535 .unwrap_or_default()
536 }
537
538 pub fn current_line_for(&mut self, path: &str) -> Option<usize> {
540 let line = self.current_line?;
541 let cur = self.current_path.clone()?;
542 if self.canon(path) == self.canon(&cur) {
543 Some(line)
544 } else {
545 None
546 }
547 }
548
549 pub fn shift_breakpoints(&mut self, path: &str, anchor: usize, delta: isize) {
560 if delta == 0 {
561 return;
562 }
563 let path = self.canon(path);
564 let Some(list) = self.breakpoints.get_mut(&path) else {
565 return;
566 };
567 if delta > 0 {
568 let d = delta as usize;
569 for b in list.iter_mut() {
570 if b.line > anchor {
571 b.line += d;
572 }
573 }
574 } else {
575 let d = (-delta) as usize;
576 list.retain_mut(|b| {
578 if b.line < anchor {
579 return true;
580 }
581 if b.line < anchor + d {
582 return false; }
584 b.line -= d;
585 true
586 });
587 }
588 list.sort_by_key(|b| b.line);
589 list.dedup_by_key(|b| b.line);
590 if list.is_empty() {
591 self.breakpoints.remove(&path);
592 }
593 if self.is_session() {
594 self.send_set_breakpoints(&path);
595 }
596 let _ = self.persist_breakpoints();
597 }
598
599 pub fn flat_bps(&self) -> Vec<(String, usize, bool)> {
601 let mut out = Vec::new();
602 let mut keys: Vec<_> = self.breakpoints.keys().cloned().collect();
603 keys.sort();
604 for k in keys {
605 if let Some(list) = self.breakpoints.get(&k) {
606 for b in list {
607 out.push((k.clone(), b.line, b.verified));
608 }
609 }
610 }
611 out
612 }
613
614 pub fn start(
618 &mut self,
619 program: &str,
620 cwd: Option<&Path>,
621 lang_hint: Option<&str>,
622 args: &[String],
623 ) -> Result<(), String> {
624 if self.is_session() {
625 return Err("Debug session already active — stop first (Shift+F5)".into());
626 }
627 self.finish_shutdown();
628 self.canon_cache.clear();
629
630 let program_path = PathBuf::from(program);
631 let abs_prog = std::fs::canonicalize(&program_path)
632 .unwrap_or_else(|_| program_path.clone());
633 let cwd = cwd
634 .map(Path::to_path_buf)
635 .or_else(|| abs_prog.parent().map(|p| p.to_path_buf()))
636 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
637
638 let lang = lang_hint
639 .map(|s| s.to_string())
640 .unwrap_or_else(|| detect_lang(&abs_prog));
641
642 if lang == "node" {
644 return self.start_node(&abs_prog.display().to_string(), Some(&cwd), args);
645 }
646
647 let (adapter_cmd, adapter_args, launch) = pick_adapter(&lang, &abs_prog, &cwd, args)?;
648
649 if lang == "rust" {
651 if let Some(bin) = launch.get("program").and_then(|p| p.as_str()) {
652 if !Path::new(bin).is_file() {
653 return self.begin_cargo_build(
654 &cwd,
655 abs_prog.display().to_string(),
656 lang,
657 args.to_vec(),
658 );
659 }
660 }
661 }
662
663 let mut child = Command::new(&adapter_cmd)
664 .args(&adapter_args)
665 .current_dir(&cwd)
666 .stdin(Stdio::piped())
667 .stdout(Stdio::piped())
668 .stderr(Stdio::piped())
669 .spawn()
670 .map_err(|e| format!("Failed to start {adapter_cmd}: {e}"))?;
671
672 let stdin = child
673 .stdin
674 .take()
675 .ok_or_else(|| "adapter stdin unavailable".to_string())?;
676 let stdout = child
677 .stdout
678 .take()
679 .ok_or_else(|| "adapter stdout unavailable".to_string())?;
680 let stderr = child.stderr.take();
681
682 let (tx, rx) = mpsc::channel();
683 thread::spawn(move || read_loop(stdout, tx));
684 drain_stderr(stderr);
685
686 self.begin_session_common(
687 Box::new(stdin),
688 rx,
689 Some(child),
690 &adapter_cmd,
691 &lang,
692 launch,
693 false,
694 Some(abs_prog.display().to_string()),
695 Some(cwd.display().to_string()),
696 args.to_vec(),
697 None,
698 );
699
700 let id = self.alloc(PendingKind::Initialize);
701 let init = json!({
702 "seq": id,
703 "type": "request",
704 "command": "initialize",
705 "arguments": {
706 "clientID": "xei",
707 "clientName": "xei",
708 "adapterID": lang,
709 "pathFormat": "path",
710 "linesStartAt1": true,
711 "columnsStartAt1": true,
712 "supportsVariableType": true,
713 "supportsVariablePaging": false,
714 "supportsRunInTerminalRequest": false,
715 "locale": "en-us"
716 }
717 });
718 self.send_json(&init);
719 Ok(())
720 }
721
722 #[allow(clippy::too_many_arguments)]
724 fn begin_session_common(
725 &mut self,
726 writer: Box<dyn Write + Send>,
727 rx: Receiver<Value>,
728 child: Option<Child>,
729 adapter_name: &str,
730 lang: &str,
731 body: Value,
732 is_attach: bool,
733 program: Option<String>,
734 cwd: Option<String>,
735 args: Vec<String>,
736 attach_tag: Option<String>,
737 ) {
738 self.writer = Some(writer);
739 self.rx = Some(rx);
740 self.child = child;
741 self.adapter_name = adapter_name.to_string();
742 self.state = DapState::Starting;
743 self.error = None;
744 self.soft_error = None;
745 self.config_sent = false;
746 self.launch_sent_at = None;
747 self.launch_body = Some(body);
748 self.is_attach = is_attach;
749 self.last_program = program;
750 self.last_cwd = cwd;
751 self.last_lang = Some(lang.to_string());
752 self.last_args = args;
753 self.last_attach = attach_tag;
754 self.panel_open = true;
755 self.build_rx = None;
756 self.build_message = None;
757 self.stack.clear();
758 self.vars.clear();
759 self.threads.clear();
760 self.children_cache.clear();
761 self.current_line = None;
762 self.current_path = None;
763 self.stopped_reason = None;
764 self.thread_id = None;
765 self.pause_requested = false;
766 self.awaiting_stack_thread = false;
767 let kind = if is_attach { "attach" } else { "launch" };
768 self.log(format!(
769 "▶ {kind} · {adapter_name} · {lang} · {}",
770 self.last_program.as_deref().unwrap_or("-")
771 ));
772 }
773
774 fn send_initialize(&mut self, adapter_id: &str) {
775 let id = self.alloc(PendingKind::Initialize);
776 self.send_json(&json!({
777 "seq": id,
778 "type": "request",
779 "command": "initialize",
780 "arguments": {
781 "clientID": "xei",
782 "clientName": "xei",
783 "adapterID": adapter_id,
784 "pathFormat": "path",
785 "linesStartAt1": true,
786 "columnsStartAt1": true,
787 "supportsVariableType": true,
788 "supportsVariablePaging": false,
789 "supportsRunInTerminalRequest": false,
790 "locale": "en-us"
791 }
792 }));
793 }
794
795 pub fn attach_pid(&mut self, pid: u32) -> Result<(), String> {
797 if self.is_session() {
798 return Err("Debug session already active — stop first (Shift+F5)".into());
799 }
800 self.finish_shutdown();
801 let adapter = ["lldb-dap", "codelldb", "lldb-vscode"]
802 .into_iter()
803 .find(|c| command_exists(c))
804 .ok_or_else(|| install_hint("rust"))?;
805 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
806 let mut child = Command::new(adapter)
807 .current_dir(&cwd)
808 .stdin(Stdio::piped())
809 .stdout(Stdio::piped())
810 .stderr(Stdio::piped())
811 .spawn()
812 .map_err(|e| format!("Failed to start {adapter}: {e}"))?;
813 let stdin = child
814 .stdin
815 .take()
816 .ok_or_else(|| "adapter stdin unavailable".to_string())?;
817 let stdout = child
818 .stdout
819 .take()
820 .ok_or_else(|| "adapter stdout unavailable".to_string())?;
821 drain_stderr(child.stderr.take());
822 let (tx, rx) = mpsc::channel();
823 thread::spawn(move || read_loop(stdout, tx));
824 let body = json!({
825 "name": format!("Attach PID {pid}"),
826 "type": "lldb",
827 "request": "attach",
828 "pid": pid,
829 "stopOnEntry": false
830 });
831 self.begin_session_common(
832 Box::new(stdin),
833 rx,
834 Some(child),
835 adapter,
836 "native",
837 body,
838 true,
839 Some(format!("pid:{pid}")),
840 Some(cwd.display().to_string()),
841 Vec::new(),
842 Some(format!("pid:{pid}")),
843 );
844 self.send_initialize("lldb");
845 Ok(())
846 }
847
848 pub fn attach_port(
854 &mut self,
855 port: u16,
856 lang_hint: Option<&str>,
857 host: Option<&str>,
858 ) -> Result<(), String> {
859 if self.is_session() {
860 return Err("Debug session already active — stop first (Shift+F5)".into());
861 }
862 self.finish_shutdown();
863 let host = host.unwrap_or("127.0.0.1");
864 let lang = lang_hint.unwrap_or("python");
865 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
866
867 match lang {
868 "node" | "javascript" | "typescript" => {
869 self.start_js_debug_tcp_session(
872 json!({
873 "name": format!("Attach Node :{port}"),
874 "type": "pwa-node",
875 "request": "attach",
876 "address": host,
877 "port": port,
878 "localRoot": cwd.display().to_string(),
879 "skipFiles": ["<node_internals>/**"]
880 }),
881 true,
882 Some(format!("port:{port}")),
883 Some(cwd.display().to_string()),
884 Vec::new(),
885 Some(format!("port:{port}")),
886 )
887 }
888 "python" | "debugpy" => {
889 let py = if command_exists("python3") {
890 "python3"
891 } else if command_exists("python") {
892 "python"
893 } else {
894 return Err(install_hint("python"));
895 };
896 let mut child = Command::new(py)
897 .args(["-m", "debugpy.adapter"])
898 .current_dir(&cwd)
899 .stdin(Stdio::piped())
900 .stdout(Stdio::piped())
901 .stderr(Stdio::piped())
902 .spawn()
903 .map_err(|e| format!("Failed to start debugpy.adapter: {e}"))?;
904 let stdin = child
905 .stdin
906 .take()
907 .ok_or_else(|| "adapter stdin unavailable".to_string())?;
908 let stdout = child
909 .stdout
910 .take()
911 .ok_or_else(|| "adapter stdout unavailable".to_string())?;
912 drain_stderr(child.stderr.take());
913 let (tx, rx) = mpsc::channel();
914 thread::spawn(move || read_loop(stdout, tx));
915 let body = json!({
916 "name": format!("Python Attach :{port}"),
917 "type": "python",
918 "request": "attach",
919 "connect": { "host": host, "port": port },
920 "justMyCode": true
921 });
922 self.begin_session_common(
923 Box::new(stdin),
924 rx,
925 Some(child),
926 "debugpy",
927 "python",
928 body,
929 true,
930 Some(format!("port:{port}")),
931 Some(cwd.display().to_string()),
932 Vec::new(),
933 Some(format!("port:{port}")),
934 );
935 self.send_initialize("python");
936 Ok(())
937 }
938 _ => {
939 let adapter = ["lldb-dap", "codelldb"]
942 .into_iter()
943 .find(|c| command_exists(c))
944 .ok_or_else(|| {
945 String::from(
946 "No attach adapter. Use `:DapAttach pid <n>` or python/node port attach",
947 )
948 })?;
949 let mut child = Command::new(adapter)
950 .current_dir(&cwd)
951 .stdin(Stdio::piped())
952 .stdout(Stdio::piped())
953 .stderr(Stdio::piped())
954 .spawn()
955 .map_err(|e| format!("Failed to start {adapter}: {e}"))?;
956 let stdin = child
957 .stdin
958 .take()
959 .ok_or_else(|| "adapter stdin unavailable".to_string())?;
960 let stdout = child
961 .stdout
962 .take()
963 .ok_or_else(|| "adapter stdout unavailable".to_string())?;
964 drain_stderr(child.stderr.take());
965 let (tx, rx) = mpsc::channel();
966 thread::spawn(move || read_loop(stdout, tx));
967 let body = json!({
968 "name": format!("Attach :{port}"),
969 "type": "lldb",
970 "request": "attach",
971 "attachCommands": [format!("process connect connect://{host}:{port}")]
972 });
973 self.begin_session_common(
974 Box::new(stdin),
975 rx,
976 Some(child),
977 adapter,
978 lang,
979 body,
980 true,
981 Some(format!("port:{port}")),
982 Some(cwd.display().to_string()),
983 Vec::new(),
984 Some(format!("port:{port}")),
985 );
986 self.send_initialize(adapter);
987 Ok(())
988 }
989 }
990 }
991
992 pub fn start_node(
994 &mut self,
995 program: &str,
996 cwd: Option<&Path>,
997 args: &[String],
998 ) -> Result<(), String> {
999 if self.is_session() {
1000 return Err("Debug session already active — stop first (Shift+F5)".into());
1001 }
1002 self.finish_shutdown();
1003 let program_path = PathBuf::from(program);
1004 let abs_prog = std::fs::canonicalize(&program_path)
1005 .unwrap_or_else(|_| program_path.clone());
1006 let cwd = cwd
1007 .map(Path::to_path_buf)
1008 .or_else(|| abs_prog.parent().map(|p| p.to_path_buf()))
1009 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1010 let body = json!({
1011 "name": "Launch Node",
1012 "type": "pwa-node",
1013 "request": "launch",
1014 "program": abs_prog.display().to_string(),
1015 "args": args,
1016 "cwd": cwd.display().to_string(),
1017 "console": "internalConsole",
1018 "skipFiles": ["<node_internals>/**"]
1019 });
1020 self.start_js_debug_tcp_session(
1021 body,
1022 false,
1023 Some(abs_prog.display().to_string()),
1024 Some(cwd.display().to_string()),
1025 args.to_vec(),
1026 None,
1027 )
1028 }
1029
1030 fn start_js_debug_tcp_session(
1032 &mut self,
1033 body: Value,
1034 is_attach: bool,
1035 program: Option<String>,
1036 cwd: Option<String>,
1037 args: Vec<String>,
1038 attach_tag: Option<String>,
1039 ) -> Result<(), String> {
1040 let port = free_localhost_port().ok_or_else(|| "No free TCP port for js-debug".to_string())?;
1041 let adapter_cmd = if command_exists("js-debug-adapter") {
1042 "js-debug-adapter".to_string()
1043 } else if command_exists("node") {
1044 return Err(
1046 "js-debug-adapter not found. Install VS Code js-debug adapter on PATH".into(),
1047 );
1048 } else {
1049 return Err(install_hint("node"));
1050 };
1051
1052 let workdir = cwd
1053 .as_ref()
1054 .map(PathBuf::from)
1055 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1056
1057 let mut child = Command::new(&adapter_cmd)
1059 .args([format!("--server={port}")])
1060 .current_dir(&workdir)
1061 .stdin(Stdio::null())
1062 .stdout(Stdio::piped())
1063 .stderr(Stdio::piped())
1064 .spawn()
1065 .or_else(|_| {
1066 Command::new(&adapter_cmd)
1067 .arg(port.to_string())
1068 .current_dir(&workdir)
1069 .stdin(Stdio::null())
1070 .stdout(Stdio::piped())
1071 .stderr(Stdio::piped())
1072 .spawn()
1073 })
1074 .map_err(|e| format!("Failed to start {adapter_cmd}: {e}"))?;
1075
1076 drain_stderr(child.stderr.take());
1077 if let Some(out) = child.stdout.take() {
1078 thread::spawn(move || {
1080 let mut r = BufReader::new(out);
1081 let mut line = String::new();
1082 while r.read_line(&mut line).unwrap_or(0) > 0 {
1083 line.clear();
1084 }
1085 });
1086 }
1087
1088 let stream = wait_for_tcp("127.0.0.1", port, Duration::from_secs(3)).map_err(|e| {
1090 let _ = child.kill();
1091 e
1092 })?;
1093 let reader = stream
1094 .try_clone()
1095 .map_err(|e| format!("tcp clone: {e}"))?;
1096 let (tx, rx) = mpsc::channel();
1097 thread::spawn(move || read_loop(reader, tx));
1098
1099 self.begin_session_common(
1100 Box::new(stream),
1101 rx,
1102 Some(child),
1103 "js-debug",
1104 "node",
1105 body,
1106 is_attach,
1107 program,
1108 cwd.or_else(|| Some(workdir.display().to_string())),
1109 args,
1110 attach_tag,
1111 );
1112 self.send_initialize("pwa-node");
1113 Ok(())
1114 }
1115
1116 pub fn continue_exec(&mut self) {
1117 let Some(tid) = self.thread_id else {
1118 self.log("continue: no thread");
1119 return;
1120 };
1121 if self.state != DapState::Stopped {
1122 self.log("continue: not stopped");
1123 return;
1124 }
1125 let id = self.alloc(PendingKind::Continue);
1126 self.send_json(&json!({
1127 "seq": id,
1128 "type": "request",
1129 "command": "continue",
1130 "arguments": { "threadId": tid }
1131 }));
1132 self.on_resumed();
1133 self.log("→ continue");
1134 }
1135
1136 pub fn step_over(&mut self) {
1137 self.step_cmd("next", PendingKind::Next);
1138 }
1139 pub fn step_into(&mut self) {
1140 self.step_cmd("stepIn", PendingKind::StepIn);
1141 }
1142 pub fn step_out(&mut self) {
1143 self.step_cmd("stepOut", PendingKind::StepOut);
1144 }
1145
1146 fn step_cmd(&mut self, command: &str, kind: PendingKind) {
1147 let Some(tid) = self.thread_id else {
1148 self.log(format!("{command}: no thread"));
1149 return;
1150 };
1151 if self.state != DapState::Stopped {
1152 self.log(format!("{command}: not stopped"));
1153 return;
1154 }
1155 let id = self.alloc(kind);
1156 self.send_json(&json!({
1157 "seq": id,
1158 "type": "request",
1159 "command": command,
1160 "arguments": { "threadId": tid }
1161 }));
1162 self.on_resumed();
1163 self.log(format!("→ {command}"));
1164 }
1165
1166 pub fn pause(&mut self) {
1168 if self.state != DapState::Running {
1169 self.log("pause: not running");
1170 return;
1171 }
1172 if let Some(tid) = self.thread_id {
1173 let id = self.alloc(PendingKind::Pause);
1174 self.send_json(&json!({
1175 "seq": id,
1176 "type": "request",
1177 "command": "pause",
1178 "arguments": { "threadId": tid }
1179 }));
1180 self.log("→ pause");
1181 } else {
1182 self.pause_requested = true;
1184 self.request_threads();
1185 }
1186 }
1187
1188 fn on_resumed(&mut self) {
1190 self.state = DapState::Running;
1191 self.current_line = None;
1192 self.children_cache.clear();
1193 }
1194
1195 pub fn stop(&mut self) {
1198 if self.writer.is_none() {
1199 self.finish_shutdown();
1200 return;
1201 }
1202 if self.state == DapState::Ending {
1203 self.log("■ force kill");
1204 self.finish_shutdown();
1205 return;
1206 }
1207 self.state = DapState::Ending;
1208 if self.supports_terminate {
1209 let id = self.alloc(PendingKind::Terminate);
1210 self.send_json(&json!({
1211 "seq": id,
1212 "type": "request",
1213 "command": "terminate",
1214 "arguments": { "restart": false }
1215 }));
1216 } else {
1217 let id = self.alloc(PendingKind::Disconnect);
1218 self.send_json(&json!({
1219 "seq": id,
1220 "type": "request",
1221 "command": "disconnect",
1222 "arguments": { "restart": false, "terminateDebuggee": true }
1223 }));
1224 }
1225 self.shutdown_deadline = Some(Instant::now() + SHUTDOWN_GRACE);
1226 self.log("■ stopping…");
1227 }
1228
1229 pub fn restart(&mut self) -> Result<(), String> {
1231 let prog = self
1232 .last_program
1233 .clone()
1234 .ok_or_else(|| "No previous program".to_string())?;
1235 let cwd = self.last_cwd.clone().map(PathBuf::from);
1236 let lang = self.last_lang.clone();
1237 let args = self.last_args.clone();
1238 if self.is_session() || self.state == DapState::Ending {
1239 self.restart_pending = Some((prog, cwd, lang, args));
1240 self.stop();
1241 self.log("↻ restart queued");
1242 Ok(())
1243 } else {
1244 self.start(&prog, cwd.as_deref(), lang.as_deref(), &args)
1245 }
1246 }
1247
1248 fn finish_shutdown(&mut self) {
1249 self.writer = None;
1250 self.rx = None;
1251 if let Some(mut c) = self.child.take() {
1252 let _ = c.kill();
1253 let _ = c.wait();
1254 }
1255 self.pending.clear();
1256 self.config_sent = false;
1257 self.launch_sent_at = None;
1258 self.launch_body = None;
1259 self.shutdown_deadline = None;
1260 self.pause_requested = false;
1261 self.awaiting_stack_thread = false;
1262 self.thread_id = None;
1263 self.current_line = None;
1264 self.children_cache.clear();
1265 if self.build_rx.is_none() {
1267 self.build_message = None;
1268 }
1269 if self.state != DapState::Idle && self.build_rx.is_none() {
1270 self.state = DapState::Idle;
1271 self.log("session ended");
1272 }
1273 }
1274
1275 fn begin_cargo_build(
1277 &mut self,
1278 cwd: &Path,
1279 program: String,
1280 lang: String,
1281 args: Vec<String>,
1282 ) -> Result<(), String> {
1283 if !command_exists("cargo") {
1284 return Err(format!(
1285 "Binary missing and cargo not found — build the project first"
1286 ));
1287 }
1288 let (tx, rx) = mpsc::channel();
1289 let cwd_b = cwd.to_path_buf();
1290 let prog_for_resolve = program.clone();
1291 let lang_c = lang.clone();
1292 let args_c = args.clone();
1293 thread::spawn(move || {
1294 let out = Command::new("cargo")
1295 .args(["build"])
1296 .current_dir(&cwd_b)
1297 .stdout(Stdio::piped())
1298 .stderr(Stdio::piped())
1299 .output();
1300 match out {
1301 Ok(o) if o.status.success() => {
1302 let bin = resolve_rust_bin(&cwd_b, Path::new(&prog_for_resolve))
1303 .unwrap_or_else(|| {
1304 resolve_rust_bin(&cwd_b, Path::new("src/main.rs"))
1305 .unwrap_or(prog_for_resolve)
1306 });
1307 if Path::new(&bin).is_file() {
1308 let _ = tx.send(Ok((bin, cwd_b, lang_c, args_c)));
1309 } else {
1310 let _ = tx.send(Err(format!(
1311 "cargo build ok but binary not found: {bin}"
1312 )));
1313 }
1314 }
1315 Ok(o) => {
1316 let err = String::from_utf8_lossy(&o.stderr);
1317 let msg = err
1318 .lines()
1319 .rev()
1320 .find(|l| !l.trim().is_empty())
1321 .unwrap_or("cargo build failed")
1322 .to_string();
1323 let _ = tx.send(Err(msg));
1324 }
1325 Err(e) => {
1326 let _ = tx.send(Err(format!("cargo spawn: {e}")));
1327 }
1328 }
1329 });
1330 self.build_rx = Some(rx);
1331 self.build_message = Some("cargo build…".into());
1332 self.panel_open = true;
1333 self.state = DapState::Starting;
1334 self.last_program = Some(program);
1335 self.last_cwd = Some(cwd.display().to_string());
1336 self.last_lang = Some(lang);
1337 self.last_args = args;
1338 self.log("⚙ cargo build… (will launch when done)");
1339 Ok(())
1340 }
1341
1342 fn alloc(&mut self, kind: PendingKind) -> u64 {
1345 let id = self.next_id;
1346 self.next_id = self.next_id.saturating_add(1);
1347 self.pending.insert(id, kind);
1348 id
1349 }
1350
1351 fn send_json(&mut self, v: &Value) {
1352 #[cfg(test)]
1353 self.sent.push(v.clone());
1354 let body = v.to_string();
1355 if let Some(ref mut writer) = self.writer {
1356 let header = format!("Content-Length: {}\r\n\r\n", body.len());
1357 let _ = writer.write_all(header.as_bytes());
1358 let _ = writer.write_all(body.as_bytes());
1359 let _ = writer.flush();
1360 }
1361 }
1362
1363 fn send_set_breakpoints(&mut self, path: &str) {
1364 let lines = self
1365 .breakpoints
1366 .get(path)
1367 .map(|v| {
1368 v.iter()
1369 .map(|b| {
1370 let mut o = json!({ "line": b.line + 1 });
1371 if let Some(ref c) = b.condition {
1372 o["condition"] = json!(c);
1373 }
1374 if let Some(ref m) = b.log_message {
1375 o["logMessage"] = json!(m);
1376 }
1377 o
1378 })
1379 .collect::<Vec<_>>()
1380 })
1381 .unwrap_or_default();
1382 let id = self.alloc(PendingKind::SetBreakpoints(path.to_string()));
1383 self.send_json(&json!({
1384 "seq": id,
1385 "type": "request",
1386 "command": "setBreakpoints",
1387 "arguments": {
1388 "source": {
1389 "path": path,
1390 "name": Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path)
1391 },
1392 "breakpoints": lines,
1393 "sourceModified": false
1394 }
1395 }));
1396 }
1397
1398 pub fn evaluate(&mut self, expression: &str) {
1400 let expr = expression.trim();
1401 if expr.is_empty() {
1402 return;
1403 }
1404 if self.state != DapState::Stopped {
1405 self.log("eval: not stopped");
1406 return;
1407 }
1408 let frame_id = self
1409 .stack
1410 .get(self.selected_frame)
1411 .map(|f| f.id)
1412 .unwrap_or(0);
1413 self.log(format!("> {expr}"));
1414 let id = self.alloc(PendingKind::Evaluate);
1415 self.send_json(&json!({
1416 "seq": id,
1417 "type": "request",
1418 "command": "evaluate",
1419 "arguments": {
1420 "expression": expr,
1421 "frameId": frame_id,
1422 "context": "repl"
1423 }
1424 }));
1425 self.eval_input.clear();
1426 }
1427
1428 fn send_configuration(&mut self) {
1431 if self.config_sent {
1432 return;
1433 }
1434 self.config_sent = true;
1435 let paths: Vec<String> = self.breakpoints.keys().cloned().collect();
1436 for p in paths {
1437 self.send_set_breakpoints(&p);
1438 }
1439 if !self.exception_filters.is_empty() {
1440 let filters = self.exception_filters.clone();
1441 let id = self.alloc(PendingKind::ExceptionBreakpoints);
1442 self.send_json(&json!({
1443 "seq": id,
1444 "type": "request",
1445 "command": "setExceptionBreakpoints",
1446 "arguments": { "filters": filters }
1447 }));
1448 }
1449 if self.supports_config_done {
1450 let id = self.alloc(PendingKind::ConfigDone);
1451 self.send_json(&json!({
1452 "seq": id,
1453 "type": "request",
1454 "command": "configurationDone"
1455 }));
1456 }
1457 }
1458
1459 fn request_threads(&mut self) {
1460 let id = self.alloc(PendingKind::Threads);
1461 self.send_json(&json!({
1462 "seq": id,
1463 "type": "request",
1464 "command": "threads"
1465 }));
1466 }
1467
1468 fn request_stack(&mut self) {
1469 let Some(tid) = self.thread_id else { return };
1470 let id = self.alloc(PendingKind::StackTrace);
1471 self.send_json(&json!({
1472 "seq": id,
1473 "type": "request",
1474 "command": "stackTrace",
1475 "arguments": {
1476 "threadId": tid,
1477 "startFrame": 0,
1478 "levels": STACK_LEVELS
1479 }
1480 }));
1481 }
1482
1483 fn request_scopes(&mut self, frame_id: i64) {
1484 let id = self.alloc(PendingKind::Scopes);
1485 self.send_json(&json!({
1486 "seq": id,
1487 "type": "request",
1488 "command": "scopes",
1489 "arguments": { "frameId": frame_id }
1490 }));
1491 }
1492
1493 fn request_variables(&mut self, variables_reference: i64) {
1494 if variables_reference <= 0 {
1495 return;
1496 }
1497 let id = self.alloc(PendingKind::Variables(variables_reference));
1498 self.send_json(&json!({
1499 "seq": id,
1500 "type": "request",
1501 "command": "variables",
1502 "arguments": { "variablesReference": variables_reference }
1503 }));
1504 }
1505
1506 pub fn select_frame(&mut self, idx: usize) {
1510 if idx >= self.stack.len() {
1511 return;
1512 }
1513 self.selected_frame = idx;
1514 self.focus_row = idx;
1515 let frame = &self.stack[idx];
1516 self.current_path = Some(frame.path.clone());
1517 self.current_line = Some(frame.line);
1518 let fid = frame.id;
1519 self.vars.clear();
1520 self.request_scopes(fid);
1521 }
1522
1523 pub fn toggle_var_at(&mut self, idx: usize) {
1525 let Some(node) = self.vars.get(idx) else {
1526 return;
1527 };
1528 if node.var_ref <= 0 {
1529 return;
1530 }
1531 if node.expanded {
1532 let depth = node.depth;
1533 self.vars[idx].expanded = false;
1534 let mut end = idx + 1;
1535 while end < self.vars.len() && self.vars[end].depth > depth {
1536 end += 1;
1537 }
1538 self.vars.drain(idx + 1..end);
1539 } else {
1540 if self.state != DapState::Stopped {
1541 return;
1543 }
1544 self.vars[idx].expanded = true;
1545 let vr = self.vars[idx].var_ref;
1546 if let Some(children) = self.children_cache.get(&vr).cloned() {
1547 self.insert_children(vr, children);
1548 } else {
1549 self.request_variables(vr);
1550 }
1551 }
1552 }
1553
1554 fn insert_children(&mut self, var_ref: i64, children: Vec<VarNode>) {
1556 let Some(pos) = self
1557 .vars
1558 .iter()
1559 .position(|n| n.var_ref == var_ref && n.expanded)
1560 else {
1561 return; };
1563 let depth = self.vars[pos].depth + 1;
1564 let mut rows = children;
1565 for r in &mut rows {
1566 r.depth = depth;
1567 r.expanded = false;
1568 }
1569 let mut end = pos + 1;
1571 while end < self.vars.len() && self.vars[end].depth > self.vars[pos].depth {
1572 end += 1;
1573 }
1574 self.vars.splice(pos + 1..end, rows);
1575 }
1576
1577 pub fn move_focus(&mut self, delta: isize) {
1579 let len = match self.pane {
1580 DebugPane::Stack => self.stack.len(),
1581 DebugPane::Variables => self.vars.len(),
1582 DebugPane::Breakpoints => self.flat_bps().len(),
1583 DebugPane::Console => self.console.len(),
1584 };
1585 if len == 0 {
1586 self.focus_row = 0;
1587 return;
1588 }
1589 let cur = self.focus_row as isize + delta;
1590 self.focus_row = cur.clamp(0, (len as isize) - 1) as usize;
1591 match self.pane {
1592 DebugPane::Stack => self.selected_frame = self.focus_row,
1593 DebugPane::Breakpoints => self.selected_bp = self.focus_row,
1594 DebugPane::Variables | DebugPane::Console => {}
1595 }
1596 }
1597
1598 pub fn set_pane(&mut self, pane: DebugPane) {
1600 self.pane = pane;
1601 self.focus_row = match pane {
1602 DebugPane::Stack => self.selected_frame.min(self.stack.len().saturating_sub(1)),
1603 DebugPane::Console => self.console.len().saturating_sub(1),
1604 _ => 0,
1605 };
1606 }
1607
1608 pub fn poll(&mut self) {
1611 if let Some(rx) = self.build_rx.take() {
1613 match rx.try_recv() {
1614 Ok(Ok((bin, cwd, lang, args))) => {
1615 self.build_message = None;
1616 self.log(format!("✓ cargo build ok · launching {bin}"));
1617 if let Err(e) = self.start(&bin, Some(&cwd), Some(&lang), &args) {
1618 self.soft_error = Some(e.clone());
1619 self.log(format!("✗ launch after build: {e}"));
1620 self.state = DapState::Idle;
1621 }
1622 }
1623 Ok(Err(e)) => {
1624 self.build_message = None;
1625 self.build_rx = None;
1626 self.state = DapState::Idle;
1627 self.soft_error = Some(e.clone());
1628 self.log(format!("✗ cargo build: {e}"));
1629 }
1630 Err(TryRecvError::Empty) => {
1631 self.build_rx = Some(rx);
1632 }
1633 Err(TryRecvError::Disconnected) => {
1634 self.build_message = None;
1635 self.state = DapState::Idle;
1636 }
1637 }
1638 }
1639 if let Some(deadline) = self.shutdown_deadline {
1641 if Instant::now() >= deadline {
1642 self.log("■ grace expired — killing adapter");
1643 self.finish_shutdown();
1644 }
1645 }
1646 if !self.config_sent
1648 && self.writer.is_some()
1649 && self
1650 .launch_sent_at
1651 .map(|t| t.elapsed() >= CONFIG_FALLBACK)
1652 .unwrap_or(false)
1653 {
1654 self.log("no initialized event — sending configuration anyway");
1655 self.send_configuration();
1656 }
1657 if self.state == DapState::Idle {
1659 if let Some((prog, cwd, lang, args)) = self.restart_pending.take() {
1660 if let Err(e) = self.start(&prog, cwd.as_deref(), lang.as_deref(), &args) {
1661 self.log(format!("restart failed: {e}"));
1662 }
1663 }
1664 }
1665
1666 let mut batch = Vec::new();
1667 if let Some(ref rx) = self.rx {
1668 loop {
1669 match rx.try_recv() {
1670 Ok(m) => batch.push(m),
1671 Err(TryRecvError::Empty) => break,
1672 Err(TryRecvError::Disconnected) => {
1673 if self.state == DapState::Starting {
1674 let hint = self
1675 .last_lang
1676 .as_deref()
1677 .map(install_hint)
1678 .unwrap_or_default();
1679 self.error =
1680 Some(format!("Debug adapter exited at startup. {hint}"));
1681 self.log("adapter exited at startup");
1682 } else if self.is_session() {
1683 self.error = Some("Debug adapter disconnected".into());
1684 self.log("adapter disconnected");
1685 }
1686 self.finish_shutdown();
1687 break;
1688 }
1689 }
1690 }
1691 }
1692 for msg in batch {
1693 self.handle_msg(msg);
1694 }
1695 }
1696
1697 fn handle_msg(&mut self, v: Value) {
1698 match v.get("type").and_then(|t| t.as_str()).unwrap_or("") {
1699 "event" => self.handle_event(&v),
1700 "response" => self.handle_response(&v),
1701 "request" => self.handle_reverse_request(&v),
1702 _ => {}
1703 }
1704 }
1705
1706 fn handle_event(&mut self, v: &Value) {
1707 let event = v.get("event").and_then(|e| e.as_str()).unwrap_or("");
1708 let body = v.get("body").cloned().unwrap_or(json!({}));
1709 match event {
1710 "initialized" => {
1711 self.send_configuration();
1713 }
1714 "stopped" => {
1715 self.state = DapState::Stopped;
1716 let reason = body
1717 .get("reason")
1718 .and_then(|r| r.as_str())
1719 .unwrap_or("stopped")
1720 .to_string();
1721 self.stopped_reason = Some(reason.clone());
1722 self.children_cache.clear();
1723 if let Some(tid) = body.get("threadId").and_then(|t| t.as_i64()) {
1724 self.thread_id = Some(tid);
1725 self.log(format!("● stopped ({reason})"));
1726 self.request_stack();
1727 } else {
1728 self.log(format!("● stopped ({reason}) — resolving thread"));
1730 self.awaiting_stack_thread = true;
1731 self.request_threads();
1732 }
1733 }
1734 "continued" => {
1735 self.on_resumed();
1736 }
1737 "thread" => {
1738 let tid = body.get("threadId").and_then(|t| t.as_i64()).unwrap_or(0);
1739 match body.get("reason").and_then(|r| r.as_str()).unwrap_or("") {
1740 "started" => {
1741 if !self.threads.iter().any(|(id, _)| *id == tid) {
1742 self.threads.push((tid, format!("thread {tid}")));
1743 }
1744 }
1745 "exited" => {
1746 self.threads.retain(|(id, _)| *id != tid);
1747 if self.thread_id == Some(tid) {
1748 self.thread_id = None;
1749 }
1750 }
1751 _ => {}
1752 }
1753 }
1754 "breakpoint" => {
1755 self.apply_breakpoint_event(&body);
1756 }
1757 "terminated" => {
1758 self.log("■ terminated");
1759 self.finish_shutdown();
1760 }
1761 "exited" => {
1762 let code = body
1763 .get("exitCode")
1764 .and_then(|c| c.as_i64())
1765 .unwrap_or_default();
1766 self.log(format!("program exited with code {code}"));
1768 }
1769 "output" => {
1770 let cat = body
1771 .get("category")
1772 .and_then(|c| c.as_str())
1773 .unwrap_or("console");
1774 let out = body
1775 .get("output")
1776 .and_then(|o| o.as_str())
1777 .unwrap_or("")
1778 .trim_end()
1779 .to_string();
1780 if !out.is_empty() {
1781 for line in out.lines() {
1782 self.log(format!("[{cat}] {line}"));
1783 }
1784 }
1785 }
1786 _ => {}
1787 }
1788 }
1789
1790 fn apply_breakpoint_event(&mut self, body: &Value) {
1792 let Some(bp) = body.get("breakpoint") else {
1793 return;
1794 };
1795 let line = bp
1796 .get("line")
1797 .and_then(|l| l.as_u64())
1798 .map(|l| l.saturating_sub(1) as usize);
1799 let verified = bp
1800 .get("verified")
1801 .and_then(|x| x.as_bool())
1802 .unwrap_or(false);
1803 let message = bp
1804 .get("message")
1805 .and_then(|m| m.as_str())
1806 .unwrap_or("")
1807 .to_string();
1808 let src_path = bp
1809 .get("source")
1810 .and_then(|s| s.get("path"))
1811 .and_then(|p| p.as_str())
1812 .map(|s| s.to_string());
1813 let Some(line) = line else { return };
1814 let canon_src = src_path.map(|p| self.canon(&p));
1815 for (path, list) in self.breakpoints.iter_mut() {
1816 if canon_src.as_ref().map(|s| s == path).unwrap_or(true) {
1817 if let Some(b) = list.iter_mut().find(|b| b.line == line) {
1818 b.verified = verified;
1819 b.message = message.clone();
1820 }
1821 }
1822 }
1823 }
1824
1825 fn handle_response(&mut self, v: &Value) {
1826 let id = v.get("request_seq").and_then(|x| x.as_u64());
1827 let success = v.get("success").and_then(|s| s.as_bool()).unwrap_or(false);
1828 let command = v.get("command").and_then(|c| c.as_str()).unwrap_or("");
1829 let body = v.get("body").cloned().unwrap_or(json!({}));
1830 let kind = id.and_then(|i| self.pending.remove(&i));
1831
1832 if !success {
1833 let msg = v
1834 .get("message")
1835 .and_then(|m| m.as_str())
1836 .unwrap_or("request failed");
1837 self.log(format!("✗ {command}: {msg}"));
1838 match kind {
1839 Some(PendingKind::Initialize | PendingKind::Launch) => {
1840 self.error = Some(msg.to_string());
1841 self.finish_shutdown();
1842 }
1843 Some(PendingKind::Terminate) => {
1844 let id = self.alloc(PendingKind::Disconnect);
1846 self.send_json(&json!({
1847 "seq": id,
1848 "type": "request",
1849 "command": "disconnect",
1850 "arguments": { "restart": false, "terminateDebuggee": true }
1851 }));
1852 }
1853 _ => {}
1854 }
1855 return;
1856 }
1857
1858 match kind {
1859 Some(PendingKind::Initialize) => {
1860 self.supports_config_done = body
1861 .get("supportsConfigurationDoneRequest")
1862 .and_then(|x| x.as_bool())
1863 .unwrap_or(true);
1864 self.supports_terminate = body
1865 .get("supportsTerminateRequest")
1866 .and_then(|x| x.as_bool())
1867 .unwrap_or(false);
1868 self.exception_filters = pick_exception_filters(&body);
1869 if let Some(args) = self.launch_body.take() {
1872 let id = self.alloc(PendingKind::Launch);
1873 let cmd = if self.is_attach { "attach" } else { "launch" };
1874 self.send_json(&json!({
1875 "seq": id,
1876 "type": "request",
1877 "command": cmd,
1878 "arguments": args
1879 }));
1880 self.launch_sent_at = Some(Instant::now());
1881 self.log(format!("initialize ok → {cmd}"));
1882 }
1883 }
1884 Some(PendingKind::Launch) => {
1885 let kind = if self.is_attach { "attach" } else { "launch" };
1886 self.log(format!("{kind} ok"));
1887 if self.state == DapState::Starting {
1888 self.state = DapState::Running;
1889 }
1890 }
1891 Some(PendingKind::SetBreakpoints(path)) => {
1892 if let Some(arr) = body.get("breakpoints").and_then(|b| b.as_array()) {
1894 if let Some(list) = self.breakpoints.get_mut(&path) {
1895 for (b, resp) in list.iter_mut().zip(arr) {
1896 b.verified = resp
1897 .get("verified")
1898 .and_then(|x| x.as_bool())
1899 .unwrap_or(false);
1900 b.message = resp
1901 .get("message")
1902 .and_then(|m| m.as_str())
1903 .unwrap_or("")
1904 .to_string();
1905 if let Some(l) = resp.get("line").and_then(|l| l.as_u64()) {
1907 b.line = l.saturating_sub(1) as usize;
1908 }
1909 }
1910 list.sort_by_key(|b| b.line);
1911 list.dedup_by_key(|b| b.line);
1912 }
1913 }
1914 }
1915 Some(PendingKind::Threads) => {
1916 self.threads = body
1917 .get("threads")
1918 .and_then(|t| t.as_array())
1919 .map(|arr| {
1920 arr.iter()
1921 .map(|t| {
1922 (
1923 t.get("id").and_then(|i| i.as_i64()).unwrap_or(0),
1924 t.get("name")
1925 .and_then(|n| n.as_str())
1926 .unwrap_or("thread")
1927 .to_string(),
1928 )
1929 })
1930 .collect()
1931 })
1932 .unwrap_or_default();
1933 if self.thread_id.is_none() {
1934 self.thread_id = self.threads.first().map(|(id, _)| *id);
1935 }
1936 if self.awaiting_stack_thread {
1937 self.awaiting_stack_thread = false;
1938 self.request_stack();
1939 }
1940 if self.pause_requested {
1941 self.pause_requested = false;
1942 if self.state == DapState::Running {
1943 self.pause();
1944 }
1945 }
1946 }
1947 Some(PendingKind::StackTrace) => {
1948 self.stack.clear();
1949 if let Some(arr) = body.get("stackFrames").and_then(|s| s.as_array()) {
1950 for f in arr {
1951 self.stack.push(StackFrameInfo {
1952 id: f.get("id").and_then(|x| x.as_i64()).unwrap_or(0),
1953 name: f
1954 .get("name")
1955 .and_then(|x| x.as_str())
1956 .unwrap_or("??")
1957 .to_string(),
1958 path: f
1959 .get("source")
1960 .and_then(|s| s.get("path"))
1961 .and_then(|p| p.as_str())
1962 .unwrap_or("")
1963 .to_string(),
1964 line: f
1965 .get("line")
1966 .and_then(|l| l.as_u64())
1967 .unwrap_or(1)
1968 .saturating_sub(1) as usize,
1969 column: f
1970 .get("column")
1971 .and_then(|c| c.as_u64())
1972 .unwrap_or(1)
1973 .saturating_sub(1) as usize,
1974 });
1975 }
1976 }
1977 if let Some(top) = self.stack.first() {
1978 self.current_path = Some(top.path.clone());
1979 self.current_line = Some(top.line);
1980 self.selected_frame = 0;
1981 if self.pane == DebugPane::Stack {
1982 self.focus_row = 0;
1983 }
1984 self.location_dirty = true;
1985 let fid = top.id;
1986 self.vars.clear();
1987 self.request_scopes(fid);
1988 }
1989 }
1990 Some(PendingKind::Scopes) => {
1991 self.vars.clear();
1992 if let Some(arr) = body.get("scopes").and_then(|s| s.as_array()) {
1993 for s in arr {
1994 self.vars.push(VarNode {
1995 name: s
1996 .get("name")
1997 .and_then(|n| n.as_str())
1998 .unwrap_or("scope")
1999 .to_string(),
2000 value: String::new(),
2001 typ: String::new(),
2002 var_ref: s
2003 .get("variablesReference")
2004 .and_then(|r| r.as_i64())
2005 .unwrap_or(0),
2006 depth: 0,
2007 expanded: false,
2008 is_scope: true,
2009 });
2010 }
2011 }
2012 if let Some(first) = self.vars.first_mut() {
2014 if first.var_ref > 0 {
2015 first.expanded = true;
2016 let vr = first.var_ref;
2017 self.request_variables(vr);
2018 }
2019 }
2020 if self.pane == DebugPane::Variables {
2021 self.focus_row = 0;
2022 }
2023 }
2024 Some(PendingKind::Variables(var_ref)) => {
2025 let children: Vec<VarNode> = body
2026 .get("variables")
2027 .and_then(|s| s.as_array())
2028 .map(|arr| {
2029 arr.iter()
2030 .map(|var| VarNode {
2031 name: var
2032 .get("name")
2033 .and_then(|n| n.as_str())
2034 .unwrap_or("?")
2035 .to_string(),
2036 value: var
2037 .get("value")
2038 .and_then(|n| n.as_str())
2039 .unwrap_or("")
2040 .to_string(),
2041 typ: var
2042 .get("type")
2043 .and_then(|n| n.as_str())
2044 .unwrap_or("")
2045 .to_string(),
2046 var_ref: var
2047 .get("variablesReference")
2048 .and_then(|r| r.as_i64())
2049 .unwrap_or(0),
2050 depth: 0,
2051 expanded: false,
2052 is_scope: false,
2053 })
2054 .collect()
2055 })
2056 .unwrap_or_default();
2057 self.children_cache.insert(var_ref, children.clone());
2058 self.insert_children(var_ref, children);
2059 }
2060 Some(
2061 PendingKind::Continue
2062 | PendingKind::Next
2063 | PendingKind::StepIn
2064 | PendingKind::StepOut
2065 | PendingKind::Pause,
2066 ) => {
2067 }
2069 Some(PendingKind::Terminate | PendingKind::Disconnect) => {
2070 }
2072 Some(PendingKind::Evaluate) => {
2073 let result = body
2074 .get("result")
2075 .and_then(|r| r.as_str())
2076 .unwrap_or("(no result)");
2077 let typ = body
2078 .get("type")
2079 .and_then(|t| t.as_str())
2080 .unwrap_or("");
2081 if typ.is_empty() {
2082 self.log(format!("= {result}"));
2083 } else {
2084 self.log(format!("= {result} ({typ})"));
2085 }
2086 }
2087 Some(PendingKind::ExceptionBreakpoints | PendingKind::ConfigDone) | None => {}
2088 }
2089 }
2090
2091 fn handle_reverse_request(&mut self, v: &Value) {
2092 let command = v.get("command").and_then(|c| c.as_str()).unwrap_or("");
2094 let seq = v.get("seq").and_then(|s| s.as_u64()).unwrap_or(0);
2095 self.log(format!("← reverse request {command} (unsupported)"));
2096 let id = self.next_id;
2097 self.next_id += 1;
2098 self.send_json(&json!({
2099 "seq": id,
2100 "type": "response",
2101 "request_seq": seq,
2102 "success": false,
2103 "command": command,
2104 "message": "not supported by xei"
2105 }));
2106 }
2107
2108 #[cfg(test)]
2112 fn test_session(&mut self, launch_body: Value) {
2113 self.state = DapState::Starting;
2114 self.launch_body = Some(launch_body);
2115 self.adapter_name = "mock".into();
2116 }
2118
2119 #[cfg(test)]
2120 fn sent_commands(&self) -> Vec<String> {
2121 self.sent
2122 .iter()
2123 .filter_map(|v| v.get("command").and_then(|c| c.as_str()))
2124 .map(|s| s.to_string())
2125 .collect()
2126 }
2127}
2128
2129fn read_loop<R: Read>(stdout: R, tx: mpsc::Sender<Value>) {
2132 let mut reader = BufReader::new(stdout);
2133 loop {
2134 let mut content_length: Option<usize> = None;
2135 loop {
2136 let mut line = String::new();
2137 match reader.read_line(&mut line) {
2138 Ok(0) => return,
2139 Ok(_) => {}
2140 Err(_) => return,
2141 }
2142 let t = line.trim_end();
2143 if t.is_empty() {
2144 break;
2145 }
2146 if let Some(rest) = t.strip_prefix("Content-Length:") {
2147 content_length = rest.trim().parse().ok();
2148 }
2149 }
2150 let Some(len) = content_length else {
2151 continue;
2152 };
2153 let mut buf = vec![0u8; len];
2154 if reader.read_exact(&mut buf).is_err() {
2155 return;
2156 }
2157 let Ok(v) = serde_json::from_slice::<Value>(&buf) else {
2159 continue;
2160 };
2161 if tx.send(v).is_err() {
2162 return;
2163 }
2164 }
2165}
2166
2167fn detect_lang(path: &Path) -> String {
2170 match path
2171 .extension()
2172 .and_then(|e| e.to_str())
2173 .unwrap_or("")
2174 .to_ascii_lowercase()
2175 .as_str()
2176 {
2177 "py" | "pyw" => "python".into(),
2178 "rs" => "rust".into(),
2179 "go" => "go".into(),
2180 "c" | "h" | "cc" | "cpp" | "cxx" | "hpp" => "cpp".into(),
2181 "js" | "mjs" | "cjs" | "ts" | "tsx" => "node".into(),
2182 "rb" => "ruby".into(),
2183 _ => "unknown".into(),
2184 }
2185}
2186
2187fn pick_exception_filters(caps: &Value) -> Vec<String> {
2190 let Some(arr) = caps
2191 .get("exceptionBreakpointFilters")
2192 .and_then(|f| f.as_array())
2193 else {
2194 return Vec::new();
2195 };
2196 let defaults: Vec<String> = arr
2197 .iter()
2198 .filter(|f| f.get("default").and_then(|d| d.as_bool()).unwrap_or(false))
2199 .filter_map(|f| f.get("filter").and_then(|s| s.as_str()))
2200 .map(|s| s.to_string())
2201 .collect();
2202 if !defaults.is_empty() {
2203 return defaults;
2204 }
2205 arr.iter()
2206 .filter_map(|f| f.get("filter").and_then(|s| s.as_str()))
2207 .filter(|s| *s == "uncaught")
2208 .map(|s| s.to_string())
2209 .collect()
2210}
2211
2212fn pick_adapter(
2213 lang: &str,
2214 program: &Path,
2215 cwd: &Path,
2216 args: &[String],
2217) -> Result<(String, Vec<String>, Value), String> {
2218 let prog_s = program.display().to_string();
2219 let cwd_s = cwd.display().to_string();
2220 match lang {
2221 "python" => {
2222 let py = if command_exists("python3") {
2223 "python3"
2224 } else if command_exists("python") {
2225 "python"
2226 } else {
2227 return Err(install_hint(lang));
2228 };
2229 Ok((
2230 py.into(),
2231 vec!["-m".into(), "debugpy.adapter".into()],
2232 json!({
2233 "name": "Python: current file",
2234 "type": "python",
2235 "request": "launch",
2236 "program": prog_s,
2237 "args": args,
2238 "cwd": cwd_s,
2239 "console": "internalConsole",
2240 "justMyCode": true,
2241 "stopOnEntry": false
2242 }),
2243 ))
2244 }
2245 "go" => {
2246 if !command_exists("dlv") {
2247 return Err(install_hint(lang));
2248 }
2249 Ok((
2250 "dlv".into(),
2251 vec!["dap".into()],
2252 json!({
2253 "name": "Launch Go",
2254 "type": "go",
2255 "request": "launch",
2256 "mode": "debug",
2257 "program": prog_s,
2258 "args": args,
2259 "cwd": cwd_s
2260 }),
2261 ))
2262 }
2263 "rust" | "cpp" | "c" => {
2264 let adapter = ["lldb-dap", "codelldb", "lldb-vscode"]
2265 .into_iter()
2266 .find(|c| command_exists(c))
2267 .ok_or_else(|| install_hint(lang))?;
2268 let program_bin = if lang == "rust"
2270 && program.extension().and_then(|e| e.to_str()) == Some("rs")
2271 {
2272 resolve_rust_bin(cwd, program).unwrap_or_else(|| prog_s.clone())
2273 } else {
2274 prog_s.clone()
2275 };
2276 Ok((
2278 adapter.into(),
2279 vec![],
2280 json!({
2281 "name": "Launch",
2282 "type": "lldb",
2283 "request": "launch",
2284 "program": program_bin,
2285 "args": args,
2286 "cwd": cwd_s,
2287 "stopOnEntry": false
2288 }),
2289 ))
2290 }
2291 "node" => {
2292 Err("Node debugging uses TCP transport — call start_node".into())
2294 }
2295 _ => {
2296 let adapter = ["lldb-dap", "codelldb"]
2298 .into_iter()
2299 .find(|c| command_exists(c))
2300 .ok_or_else(|| install_hint(lang))?;
2301 if !program.is_file() {
2302 return Err(format!("Not an executable file: {prog_s}"));
2303 }
2304 Ok((
2305 adapter.into(),
2306 vec![],
2307 json!({
2308 "name": "Launch",
2309 "type": "lldb",
2310 "request": "launch",
2311 "program": prog_s,
2312 "args": args,
2313 "cwd": cwd_s
2314 }),
2315 ))
2316 }
2317 }
2318}
2319
2320fn resolve_rust_bin(cwd: &Path, src: &Path) -> Option<String> {
2321 let mut dir = cwd.to_path_buf();
2323 for _ in 0..8 {
2324 let cargo = dir.join("Cargo.toml");
2325 if cargo.is_file() {
2326 if let Ok(text) = std::fs::read_to_string(&cargo) {
2327 if let Some(name) = parse_cargo_name(&text) {
2328 return Some(dir.join("target/debug").join(&name).display().to_string());
2329 }
2330 }
2331 break;
2332 }
2333 if !dir.pop() {
2334 break;
2335 }
2336 }
2337 let stem = src.file_stem()?.to_str()?;
2338 Some(cwd.join("target/debug").join(stem).display().to_string())
2339}
2340
2341fn parse_cargo_name(toml: &str) -> Option<String> {
2342 let mut in_package = false;
2343 for line in toml.lines() {
2344 let t = line.trim();
2345 if t.starts_with('[') {
2346 in_package = t == "[package]";
2347 continue;
2348 }
2349 if in_package {
2350 if let Some(rest) = t.strip_prefix("name") {
2351 let rest = rest.trim().trim_start_matches('=').trim();
2352 let name = rest.trim_matches('"').trim_matches('\'').to_string();
2353 if !name.is_empty() {
2354 return Some(name);
2355 }
2356 }
2357 }
2358 }
2359 None
2360}
2361
2362fn install_hint(lang: &str) -> String {
2363 match lang {
2364 "python" => "No Python DAP adapter. Install: pip install debugpy".into(),
2365 "go" => "No Go DAP adapter. Install: go install github.com/go-delve/delve/cmd/dlv@latest".into(),
2366 "rust" | "cpp" | "c" => {
2367 "No native DAP adapter. Install lldb-dap (LLVM) or CodeLLDB".into()
2368 }
2369 "node" => "No Node DAP adapter (js-debug-adapter) found".into(),
2370 _ => format!(
2371 "No DAP adapter for `{lang}`. Install debugpy / dlv / lldb-dap for your language"
2372 ),
2373 }
2374}
2375
2376fn command_exists(cmd: &str) -> bool {
2377 if cmd.contains('/') {
2378 return Path::new(cmd).is_file();
2379 }
2380 let Ok(path) = std::env::var("PATH") else {
2381 return false;
2382 };
2383 for dir in std::env::split_paths(&path) {
2384 let p = dir.join(cmd);
2385 if p.is_file() {
2386 return true;
2387 }
2388 let p_exe = dir.join(format!("{cmd}.exe"));
2390 if p_exe.is_file() {
2391 return true;
2392 }
2393 }
2394 false
2395}
2396
2397fn drain_stderr(stderr: Option<std::process::ChildStderr>) {
2398 if let Some(err) = stderr {
2399 thread::spawn(move || {
2400 let mut r = BufReader::new(err);
2401 let mut line = String::new();
2402 while r.read_line(&mut line).unwrap_or(0) > 0 {
2403 line.clear();
2404 }
2405 });
2406 }
2407}
2408
2409fn free_localhost_port() -> Option<u16> {
2410 let listener = std::net::TcpListener::bind("127.0.0.1:0").ok()?;
2411 listener.local_addr().ok().map(|a| a.port())
2412}
2413
2414fn wait_for_tcp(host: &str, port: u16, timeout: Duration) -> Result<TcpStream, String> {
2415 let start = Instant::now();
2416 let mut last_err = String::from("connect failed");
2417 while start.elapsed() < timeout {
2418 match TcpStream::connect((host, port)) {
2419 Ok(s) => {
2420 let _ = s.set_nodelay(true);
2421 return Ok(s);
2422 }
2423 Err(e) => {
2424 last_err = e.to_string();
2425 thread::sleep(Duration::from_millis(40));
2426 }
2427 }
2428 }
2429 Err(format!("TCP {host}:{port} not ready: {last_err}"))
2430}
2431
2432#[derive(Debug, Clone)]
2436pub struct LaunchConfig {
2437 pub name: String,
2438 pub request: String,
2439 pub program: String,
2440 pub args: Vec<String>,
2441 pub cwd: Option<String>,
2442 pub env: Vec<(String, String)>,
2443 pub adapter_type: String,
2445 pub pid: Option<u32>,
2447 pub port: Option<u16>,
2449 pub host: Option<String>,
2451}
2452
2453pub fn load_launch_configs(hint: Option<&Path>) -> Vec<LaunchConfig> {
2455 let start = hint
2456 .map(Path::to_path_buf)
2457 .or_else(|| std::env::current_dir().ok())
2458 .unwrap_or_else(|| PathBuf::from("."));
2459 let mut dir = if start.is_file() {
2460 start.parent().unwrap_or(Path::new(".")).to_path_buf()
2461 } else {
2462 start
2463 };
2464 for _ in 0..12 {
2465 let candidate = dir.join(".vscode").join("launch.json");
2466 if candidate.is_file() {
2467 if let Ok(text) = std::fs::read_to_string(&candidate) {
2468 let cleaned = strip_jsonc_comments(&text);
2470 if let Ok(v) = serde_json::from_str::<Value>(&cleaned) {
2471 return parse_launch_configs(&v, &dir);
2472 }
2473 }
2474 }
2475 if !dir.pop() {
2476 break;
2477 }
2478 }
2479 Vec::new()
2480}
2481
2482fn strip_jsonc_comments(text: &str) -> String {
2483 let mut out = String::with_capacity(text.len());
2484 let mut chars = text.chars().peekable();
2485 let mut in_str = false;
2486 let mut escape = false;
2487 while let Some(c) = chars.next() {
2488 if in_str {
2489 out.push(c);
2490 if escape {
2491 escape = false;
2492 } else if c == '\\' {
2493 escape = true;
2494 } else if c == '"' {
2495 in_str = false;
2496 }
2497 continue;
2498 }
2499 if c == '"' {
2500 in_str = true;
2501 out.push(c);
2502 continue;
2503 }
2504 if c == '/' && chars.peek() == Some(&'/') {
2505 while let Some(n) = chars.next() {
2507 if n == '\n' {
2508 out.push('\n');
2509 break;
2510 }
2511 }
2512 continue;
2513 }
2514 if c == '/' && chars.peek() == Some(&'*') {
2515 chars.next();
2516 while let Some(n) = chars.next() {
2517 if n == '*' && chars.peek() == Some(&'/') {
2518 chars.next();
2519 break;
2520 }
2521 }
2522 continue;
2523 }
2524 out.push(c);
2525 }
2526 out
2527}
2528
2529fn parse_launch_configs(v: &Value, workspace: &Path) -> Vec<LaunchConfig> {
2530 let Some(arr) = v.get("configurations").and_then(|c| c.as_array()) else {
2531 return Vec::new();
2532 };
2533 let mut out = Vec::new();
2534 for c in arr {
2535 let request = c
2536 .get("request")
2537 .and_then(|r| r.as_str())
2538 .unwrap_or("launch")
2539 .to_string();
2540 if request != "launch" && request != "attach" {
2541 continue;
2542 }
2543 let name = c
2544 .get("name")
2545 .and_then(|n| n.as_str())
2546 .unwrap_or("unnamed")
2547 .to_string();
2548 let program = c
2549 .get("program")
2550 .and_then(|p| p.as_str())
2551 .unwrap_or("")
2552 .replace("${workspaceFolder}", &workspace.display().to_string())
2553 .replace("${file}", "");
2554 let args = c
2555 .get("args")
2556 .and_then(|a| a.as_array())
2557 .map(|a| {
2558 a.iter()
2559 .filter_map(|x| x.as_str().map(|s| s.to_string()))
2560 .collect()
2561 })
2562 .unwrap_or_default();
2563 let cwd = c
2564 .get("cwd")
2565 .and_then(|p| p.as_str())
2566 .map(|s| {
2567 s.replace("${workspaceFolder}", &workspace.display().to_string())
2568 });
2569 let mut env = Vec::new();
2570 if let Some(obj) = c.get("env").and_then(|e| e.as_object()) {
2571 for (k, v) in obj {
2572 if let Some(s) = v.as_str() {
2573 env.push((k.clone(), s.to_string()));
2574 }
2575 }
2576 }
2577 let adapter_type = c
2578 .get("type")
2579 .and_then(|t| t.as_str())
2580 .unwrap_or("")
2581 .to_string();
2582 let pid = c
2583 .get("processId")
2584 .or_else(|| c.get("pid"))
2585 .and_then(|p| p.as_u64())
2586 .map(|p| p as u32);
2587 let port = c
2588 .get("port")
2589 .and_then(|p| p.as_u64())
2590 .or_else(|| {
2591 c.get("connect")
2592 .and_then(|o| o.get("port"))
2593 .and_then(|p| p.as_u64())
2594 })
2595 .map(|p| p as u16);
2596 let host = c
2597 .get("connect")
2598 .and_then(|o| o.get("host"))
2599 .and_then(|h| h.as_str())
2600 .or_else(|| c.get("address").and_then(|a| a.as_str()))
2601 .map(|s| s.to_string());
2602 out.push(LaunchConfig {
2603 name,
2604 request,
2605 program,
2606 args,
2607 cwd,
2608 env,
2609 adapter_type,
2610 pid,
2611 port,
2612 host,
2613 });
2614 }
2615 out
2616}
2617
2618#[cfg(test)]
2621mod tests {
2622 use super::*;
2623
2624 fn response(seq: u64, command: &str, body: Value) -> Value {
2625 json!({
2626 "type": "response",
2627 "request_seq": seq,
2628 "success": true,
2629 "command": command,
2630 "body": body
2631 })
2632 }
2633
2634 fn event(name: &str, body: Value) -> Value {
2635 json!({ "type": "event", "event": name, "body": body })
2636 }
2637
2638 fn seq_of(d: &DapClient, command: &str) -> u64 {
2640 d.sent
2641 .iter()
2642 .rev()
2643 .find(|v| v.get("command").and_then(|c| c.as_str()) == Some(command))
2644 .and_then(|v| v.get("seq").and_then(|s| s.as_u64()))
2645 .expect("request was sent")
2646 }
2647
2648 #[test]
2649 fn toggle_breakpoint_roundtrip() {
2650 let mut d = DapClient::new();
2651 assert!(d.toggle_breakpoint("/tmp/foo.py", 10));
2652 assert!(d.has_breakpoint("/tmp/foo.py", 10));
2653 assert!(!d.toggle_breakpoint("/tmp/foo.py", 10));
2654 assert!(!d.has_breakpoint("/tmp/foo.py", 10));
2655 }
2656
2657 #[test]
2658 fn condition_and_log_on_breakpoint() {
2659 let mut d = DapClient::new();
2660 d.set_breakpoint_condition("/tmp/a.py", 5, Some("x > 0".into()));
2661 d.set_breakpoint_log("/tmp/a.py", 5, Some("hit".into()));
2662 let path = d.canon("/tmp/a.py");
2663 let b = d.breakpoints.get(&path).unwrap().iter().find(|b| b.line == 5).unwrap();
2664 assert_eq!(b.condition.as_deref(), Some("x > 0"));
2665 assert_eq!(b.log_message.as_deref(), Some("hit"));
2666 }
2667
2668 #[test]
2669 fn parse_launch_json_minimal() {
2670 let j = r#"{
2671 // comment
2672 "configurations": [
2673 {
2674 "name": "Run",
2675 "type": "python",
2676 "request": "launch",
2677 "program": "${workspaceFolder}/main.py",
2678 "args": ["a", "b"]
2679 },
2680 {
2681 "name": "AttachPy",
2682 "type": "python",
2683 "request": "attach",
2684 "connect": { "host": "127.0.0.1", "port": 5678 }
2685 }
2686 ]
2687 }"#;
2688 let cleaned = strip_jsonc_comments(j);
2689 let v: Value = serde_json::from_str(&cleaned).unwrap();
2690 let cfgs = parse_launch_configs(&v, Path::new("/proj"));
2691 assert_eq!(cfgs.len(), 2);
2692 assert_eq!(cfgs[0].name, "Run");
2693 assert_eq!(cfgs[0].program, "/proj/main.py");
2694 assert_eq!(cfgs[0].args, vec!["a", "b"]);
2695 assert_eq!(cfgs[1].request, "attach");
2696 assert_eq!(cfgs[1].port, Some(5678));
2697 assert_eq!(cfgs[1].host.as_deref(), Some("127.0.0.1"));
2698 }
2699
2700 #[test]
2701 fn free_port_and_wait_helpers() {
2702 let port = free_localhost_port().expect("port");
2703 let err = wait_for_tcp("127.0.0.1", port, Duration::from_millis(80));
2705 assert!(err.is_err());
2706 }
2707
2708 #[test]
2709 fn cargo_name_parse() {
2710 let t = "[package]\nname = \"xei-core\"\nversion = \"1\"\n";
2711 assert_eq!(parse_cargo_name(t).as_deref(), Some("xei-core"));
2712 }
2713
2714 #[test]
2715 fn detect_langs() {
2716 assert_eq!(detect_lang(Path::new("a.py")), "python");
2717 assert_eq!(detect_lang(Path::new("a.rs")), "rust");
2718 assert_eq!(detect_lang(Path::new("main.go")), "go");
2719 }
2720
2721 #[test]
2722 fn state_label() {
2723 assert_eq!(DapState::Stopped.label(), "stopped");
2724 }
2725
2726 #[test]
2727 fn sequencer_launch_after_initialize_config_after_initialized_event() {
2728 let mut d = DapClient::new();
2729 d.toggle_breakpoint("/tmp/x.py", 3);
2730 d.sent.clear();
2731 d.test_session(json!({"program": "/tmp/x.py"}));
2732
2733 d.handle_msg(response(1, "initialize", json!({
2735 "supportsConfigurationDoneRequest": true,
2736 "supportsTerminateRequest": true,
2737 "exceptionBreakpointFilters": [
2738 {"filter": "raised", "label": "Raised", "default": false},
2739 {"filter": "uncaught", "label": "Uncaught", "default": true}
2740 ]
2741 })));
2742 assert!(d.sent_commands().is_empty());
2746
2747 let init_id = d.alloc(PendingKind::Initialize);
2749 d.handle_msg(response(init_id, "initialize", json!({
2750 "supportsConfigurationDoneRequest": true,
2751 "supportsTerminateRequest": true,
2752 "exceptionBreakpointFilters": [
2753 {"filter": "uncaught", "label": "Uncaught", "default": true}
2754 ]
2755 })));
2756 assert_eq!(d.sent_commands(), vec!["launch"]);
2757 assert!(d.launch_sent_at.is_some());
2758 assert!(!d.config_sent);
2759
2760 d.handle_msg(event("initialized", json!({})));
2762 let cmds = d.sent_commands();
2763 assert_eq!(
2764 cmds,
2765 vec![
2766 "launch",
2767 "setBreakpoints",
2768 "setExceptionBreakpoints",
2769 "configurationDone"
2770 ]
2771 );
2772 assert!(d.config_sent);
2773
2774 d.handle_msg(event("initialized", json!({})));
2776 assert_eq!(d.sent_commands().len(), 4);
2777
2778 let launch_seq = seq_of(&d, "launch");
2780 d.handle_msg(response(launch_seq, "launch", json!({})));
2781 assert_eq!(d.state, DapState::Running);
2782 }
2783
2784 #[test]
2785 fn stopped_without_thread_id_resolves_via_threads() {
2786 let mut d = DapClient::new();
2787 d.test_session(json!({}));
2788 d.state = DapState::Running;
2789 d.sent.clear();
2790
2791 d.handle_msg(event("stopped", json!({ "reason": "pause" })));
2792 assert_eq!(d.state, DapState::Stopped);
2793 assert_eq!(d.sent_commands(), vec!["threads"]);
2794
2795 let tseq = seq_of(&d, "threads");
2796 d.handle_msg(response(
2797 tseq,
2798 "threads",
2799 json!({ "threads": [{"id": 7, "name": "main"}] }),
2800 ));
2801 assert_eq!(d.thread_id, Some(7));
2802 assert_eq!(d.threads, vec![(7, "main".to_string())]);
2803 assert_eq!(d.sent_commands(), vec!["threads", "stackTrace"]);
2804 }
2805
2806 #[test]
2807 fn stack_scopes_variables_build_tree() {
2808 let mut d = DapClient::new();
2809 d.test_session(json!({}));
2810 d.state = DapState::Stopped;
2811 d.thread_id = Some(1);
2812 d.sent.clear();
2813
2814 d.request_stack();
2815 let sseq = seq_of(&d, "stackTrace");
2816 d.handle_msg(response(sseq, "stackTrace", json!({
2817 "stackFrames": [
2818 {"id": 100, "name": "main", "line": 12, "column": 1,
2819 "source": {"path": "/tmp/x.py"}}
2820 ]
2821 })));
2822 assert_eq!(d.stack.len(), 1);
2823 assert_eq!(d.current_line, Some(11));
2824 assert!(d.location_dirty);
2825
2826 let scseq = seq_of(&d, "scopes");
2827 d.handle_msg(response(scseq, "scopes", json!({
2828 "scopes": [
2829 {"name": "Locals", "variablesReference": 200, "expensive": false},
2830 {"name": "Globals", "variablesReference": 300, "expensive": true}
2831 ]
2832 })));
2833 assert_eq!(d.vars.len(), 2);
2835 assert!(d.vars[0].is_scope && d.vars[0].expanded);
2836
2837 let vseq = seq_of(&d, "variables");
2838 d.handle_msg(response(vseq, "variables", json!({
2839 "variables": [
2840 {"name": "x", "value": "1", "type": "int", "variablesReference": 0},
2841 {"name": "items", "value": "[…]", "type": "list", "variablesReference": 400}
2842 ]
2843 })));
2844 assert_eq!(d.vars.len(), 4);
2845 assert_eq!(d.vars[1].name, "x");
2846 assert_eq!(d.vars[1].depth, 1);
2847 assert_eq!(d.vars[2].var_ref, 400);
2848
2849 d.toggle_var_at(0);
2851 assert_eq!(d.vars.len(), 2);
2852 let n_before = d.sent.len();
2854 d.toggle_var_at(0);
2855 assert_eq!(d.vars.len(), 4);
2856 assert_eq!(d.sent.len(), n_before);
2857 }
2858
2859 #[test]
2860 fn graceful_stop_waits_for_terminated() {
2861 let mut d = DapClient::new();
2862 d.test_session(json!({}));
2863 d.state = DapState::Running;
2864 d.supports_terminate = true;
2865 d.sent.clear();
2868 d.state = DapState::Running;
2869 d.shutdown_deadline = None;
2870 d.stop();
2872 assert_eq!(d.state, DapState::Idle);
2873 d.state = DapState::Ending;
2875 d.handle_msg(event("terminated", json!({})));
2876 assert_eq!(d.state, DapState::Idle);
2877 }
2878
2879 #[test]
2880 fn breakpoint_event_updates_verified() {
2881 let mut d = DapClient::new();
2882 d.toggle_breakpoint("/tmp/x.py", 5);
2883 assert!(!d.flat_bps()[0].2);
2884 d.handle_msg(event("breakpoint", json!({
2885 "reason": "changed",
2886 "breakpoint": { "line": 6, "verified": true }
2887 })));
2888 assert!(d.flat_bps()[0].2);
2889 }
2890
2891 #[test]
2892 fn set_breakpoints_response_slides_lines() {
2893 let mut d = DapClient::new();
2894 d.toggle_breakpoint("/tmp/x.py", 4); let path = d.breakpoints.keys().next().unwrap().clone();
2896 let id = d.alloc(PendingKind::SetBreakpoints(path.clone()));
2897 d.handle_msg(response(id, "setBreakpoints", json!({
2898 "breakpoints": [ {"verified": true, "line": 7} ]
2899 })));
2900 let list = &d.breakpoints[&path];
2901 assert_eq!(list[0].line, 6); assert!(list[0].verified);
2903 }
2904
2905 #[test]
2906 fn shift_breakpoints_tracks_edits() {
2907 let mut d = DapClient::new();
2908 for l in [2usize, 5, 9] {
2909 d.toggle_breakpoint("/tmp/x.py", l);
2910 }
2911 d.shift_breakpoints("/tmp/x.py", 3, 2);
2913 assert_eq!(d.lines_for("/tmp/x.py"), vec![2, 7, 11]);
2914 d.shift_breakpoints("/tmp/x.py", 5, -3);
2916 assert_eq!(d.lines_for("/tmp/x.py"), vec![2, 8]);
2917 }
2918
2919 #[test]
2920 fn config_fallback_fires_without_initialized_event() {
2921 let mut d = DapClient::new();
2922 d.test_session(json!({}));
2923 let init_id = d.alloc(PendingKind::Initialize);
2924 d.handle_msg(response(init_id, "initialize", json!({})));
2925 assert!(!d.config_sent);
2926 d.launch_sent_at = Some(Instant::now() - CONFIG_FALLBACK - Duration::from_millis(1));
2928 d.config_sent = false;
2931 d.send_configuration();
2932 assert!(d.config_sent);
2933 }
2934
2935 #[test]
2936 fn console_follows_tail_when_focused_there() {
2937 let mut d = DapClient::new();
2938 d.set_pane(DebugPane::Console);
2939 d.log("one");
2940 d.log("two");
2941 assert_eq!(d.focus_row, 1);
2942 d.move_focus(-1);
2944 d.log("three");
2945 assert_eq!(d.focus_row, 0);
2946 }
2947
2948 #[test]
2949 fn exception_filter_defaults() {
2950 let caps = json!({
2951 "exceptionBreakpointFilters": [
2952 {"filter": "raised", "default": false},
2953 {"filter": "uncaught", "default": true}
2954 ]
2955 });
2956 assert_eq!(pick_exception_filters(&caps), vec!["uncaught"]);
2957 let caps2 = json!({
2958 "exceptionBreakpointFilters": [
2959 {"filter": "raised"},
2960 {"filter": "uncaught"}
2961 ]
2962 });
2963 assert_eq!(pick_exception_filters(&caps2), vec!["uncaught"]);
2964 assert!(pick_exception_filters(&json!({})).is_empty());
2965 }
2966}