1use {
25 super::{
26 highlight::highlight_rust,
27 model::{DebugSession, DebugStep, DebugTx},
28 path_label::{classify, PathLabel},
29 },
30 ratatui::{
31 backend::CrosstermBackend,
32 crossterm::{
33 event::{
34 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent,
35 KeyModifiers,
36 },
37 execute,
38 terminal::{
39 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
40 },
41 },
42 layout::{Alignment, Constraint, Direction, Layout, Rect},
43 style::{Color, Modifier, Style},
44 text::{Line, Span},
45 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
46 Frame, Terminal,
47 },
48 std::{collections::HashMap, io, path::PathBuf},
49};
50
51type DebugTerm = Terminal<CrosstermBackend<io::Stdout>>;
52
53pub fn run(session: DebugSession) -> anyhow::Result<()> {
56 if session.txs.is_empty() {
57 anyhow::bail!(
58 "no traces to debug — did your tests call `anchor_v2_testing::svm()` and complete at \
59 least one transaction?"
60 );
61 }
62
63 let backend = CrosstermBackend::new(io::stdout());
64 let terminal = Terminal::new(backend)?;
65 let mut guard = TerminalGuard::new(terminal);
66 let mut app = App::new(session);
67 loop {
68 guard.term.draw(|f| app.draw(f))?;
69 let mut flow = app.handle(event::read()?);
76 let mut drained = 0;
77 while flow == Flow::Continue && drained < 256 && event::poll(std::time::Duration::ZERO)? {
78 flow = app.handle(event::read()?);
79 drained += 1;
80 }
81 if flow == Flow::Quit {
82 break;
83 }
84 }
85 Ok(())
86}
87
88enum Screen {
89 Picker,
90 Stepper,
91}
92
93#[derive(PartialEq, Eq)]
94enum Flow {
95 Continue,
96 Quit,
97}
98
99struct App {
100 session: DebugSession,
101 screen: Screen,
102 picker: ListState,
104 current_tx: usize,
106 current_node: usize,
109 current_step: usize,
111 key_buffer: String,
113 file_cache: HashMap<PathBuf, FileEntry>,
117 highlight_cache: HashMap<(PathBuf, u32), Vec<Span<'static>>>,
120 label_cache: HashMap<PathBuf, PathLabel>,
124 picker_rows: Vec<PickerRow>,
128}
129
130#[derive(Clone)]
131enum PickerRow {
132 Header(String),
134 Tx(usize),
136}
137
138enum FileEntry {
142 Loaded(Vec<String>),
143 Missing(String),
144}
145
146impl App {
147 fn new(session: DebugSession) -> Self {
148 let mut picker_rows: Vec<PickerRow> = Vec::with_capacity(session.txs.len() * 2);
153 let mut last_test: Option<&str> = None;
154 for (i, tx) in session.txs.iter().enumerate() {
155 if last_test != Some(tx.test_name.as_str()) {
156 picker_rows.push(PickerRow::Header(tx.test_name.clone()));
157 last_test = Some(tx.test_name.as_str());
158 }
159 picker_rows.push(PickerRow::Tx(i));
160 }
161 let initial = picker_rows
162 .iter()
163 .position(|r| matches!(r, PickerRow::Tx(_)))
164 .unwrap_or(0);
165
166 let mut picker = ListState::default();
167 picker.select(Some(initial));
168 Self {
169 session,
170 screen: Screen::Picker,
171 picker,
172 current_tx: 0,
173 current_node: 0,
174 current_step: 0,
175 key_buffer: String::new(),
176 file_cache: HashMap::new(),
177 highlight_cache: HashMap::new(),
178 label_cache: HashMap::new(),
179 picker_rows,
180 }
181 }
182
183 fn draw(&mut self, f: &mut Frame<'_>) {
184 match self.screen {
185 Screen::Picker => self.draw_picker(f),
186 Screen::Stepper => self.draw_stepper(f),
187 }
188 }
189
190 fn handle(&mut self, ev: Event) -> Flow {
191 match (ev, &self.screen) {
192 (Event::Key(k), Screen::Picker) => self.handle_picker_key(k),
193 (Event::Key(k), Screen::Stepper) => self.handle_stepper_key(k),
194 _ => Flow::Continue,
195 }
196 }
197
198 fn draw_picker(&mut self, f: &mut Frame<'_>) {
201 let area = f.area();
202 let [title, list, footer] = Layout::new(
203 Direction::Vertical,
204 [
205 Constraint::Length(3),
206 Constraint::Min(3),
207 Constraint::Length(3),
208 ],
209 )
210 .areas(area);
211
212 let title_block = Paragraph::new(Line::from(vec![
213 Span::styled("anchor debugger", Style::new().add_modifier(Modifier::BOLD)),
214 Span::raw(format!(" — {} transaction(s)", self.session.txs.len())),
215 ]))
216 .block(Block::default().borders(Borders::ALL));
217 f.render_widget(title_block, title);
218
219 let test_count = self
220 .picker_rows
221 .iter()
222 .filter(|r| matches!(r, PickerRow::Header(_)))
223 .count();
224 let items: Vec<ListItem> = self
225 .picker_rows
226 .iter()
227 .map(|row| match row {
228 PickerRow::Header(name) => {
229 ListItem::new(Line::from(vec![Span::styled(
233 name.to_string(),
234 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
235 )]))
236 .style(Style::new())
240 }
241 PickerRow::Tx(idx) => {
242 let tx = &self.session.txs[*idx];
243 let top = tx
244 .nodes
245 .first()
246 .map(|n| n.program_label.as_str())
247 .unwrap_or("");
248 let cpis = tx.nodes.len().saturating_sub(1);
249 let cpi_badge = if cpis > 0 {
250 format!(" +{cpis} CPI")
251 } else {
252 String::new()
253 };
254 ListItem::new(Line::from(vec![
255 Span::styled(" ├─ ", Style::new().fg(Color::DarkGray)),
258 Span::styled(
259 format!("tx{:<3}", tx.tx_seq),
260 Style::new().fg(Color::Yellow),
261 ),
262 Span::raw(format!(" {:>8} CU ", tx.total_cu)),
263 Span::raw(top.to_string()),
264 Span::styled(cpi_badge, Style::new().fg(Color::Magenta)),
265 ]))
266 }
267 })
268 .collect();
269
270 let title = format!(
271 " {} test(s), {} tx(s) — select one to step into ",
272 test_count,
273 self.session.txs.len()
274 );
275 let list_widget = List::new(items)
276 .block(Block::default().title(title).borders(Borders::ALL))
277 .highlight_style(
278 Style::new()
279 .bg(Color::DarkGray)
280 .add_modifier(Modifier::BOLD),
281 )
282 .highlight_symbol("> ");
283 f.render_stateful_widget(list_widget, list, &mut self.picker);
284
285 let help = Paragraph::new("j/k or ↑/↓ select enter/t open q quit")
286 .block(Block::default().borders(Borders::ALL))
287 .alignment(Alignment::Center);
288 f.render_widget(help, footer);
289 }
290
291 fn handle_picker_key(&mut self, k: KeyEvent) -> Flow {
292 match k.code {
293 KeyCode::Char('q') | KeyCode::Esc => return Flow::Quit,
294 KeyCode::Char('j') | KeyCode::Down => self.picker_next(),
295 KeyCode::Char('k') | KeyCode::Up => self.picker_prev(),
296 KeyCode::Char('g') | KeyCode::Home => {
297 self.picker.select(self.first_selectable());
298 }
299 KeyCode::Char('G') | KeyCode::End => {
300 self.picker.select(self.last_selectable());
301 }
302 KeyCode::Enter | KeyCode::Char('t') | KeyCode::Char('l') | KeyCode::Right => {
303 self.open_selected();
304 }
305 _ => {}
306 }
307 Flow::Continue
308 }
309
310 fn picker_next(&mut self) {
313 let from = self.picker.selected().unwrap_or(0);
314 let next = self
315 .picker_rows
316 .iter()
317 .enumerate()
318 .skip(from + 1)
319 .find(|(_, r)| matches!(r, PickerRow::Tx(_)))
320 .map(|(i, _)| i)
321 .unwrap_or(from);
322 self.picker.select(Some(next));
323 }
324
325 fn picker_prev(&mut self) {
326 let from = self.picker.selected().unwrap_or(0);
327 let next = self
328 .picker_rows
329 .iter()
330 .enumerate()
331 .take(from)
332 .rev()
333 .find(|(_, r)| matches!(r, PickerRow::Tx(_)))
334 .map(|(i, _)| i)
335 .unwrap_or(from);
336 self.picker.select(Some(next));
337 }
338
339 fn first_selectable(&self) -> Option<usize> {
340 self.picker_rows
341 .iter()
342 .position(|r| matches!(r, PickerRow::Tx(_)))
343 }
344
345 fn last_selectable(&self) -> Option<usize> {
346 self.picker_rows
347 .iter()
348 .rposition(|r| matches!(r, PickerRow::Tx(_)))
349 }
350
351 fn open_selected(&mut self) {
352 let Some(i) = self.picker.selected() else {
353 return;
354 };
355 if let Some(PickerRow::Tx(tx_idx)) = self.picker_rows.get(i) {
356 self.current_tx = *tx_idx;
357 self.current_node = 0;
358 self.current_step = 0;
359 self.screen = Screen::Stepper;
360 }
361 }
362
363 fn current_tx(&self) -> &DebugTx {
366 &self.session.txs[self.current_tx]
367 }
368
369 fn current_steps(&self) -> &[DebugStep] {
370 &self.current_tx().nodes[self.current_node].steps
371 }
372
373 fn draw_stepper(&mut self, f: &mut Frame<'_>) {
374 let area = f.area();
377 if area.width < 80 || area.height < 20 {
378 let msg = Paragraph::new(format!(
379 "terminal too small ({}x{}) — need at least 80x20",
380 area.width, area.height
381 ))
382 .alignment(Alignment::Center)
383 .wrap(Wrap { trim: true });
384 f.render_widget(msg, area);
385 return;
386 }
387
388 let [header, main, footer] = Layout::new(
390 Direction::Vertical,
391 [
392 Constraint::Length(4),
393 Constraint::Min(10),
394 Constraint::Length(3),
395 ],
396 )
397 .areas(area);
398
399 self.draw_stepper_header(f, header);
400
401 let [left, right] = Layout::new(
403 Direction::Horizontal,
404 [Constraint::Percentage(55), Constraint::Percentage(45)],
405 )
406 .areas(main);
407
408 self.draw_instructions(f, left);
409
410 let [right_top, right_bot] = Layout::new(
411 Direction::Vertical,
412 [Constraint::Length(14), Constraint::Min(5)],
413 )
414 .areas(right);
415 self.draw_registers(f, right_top);
416 self.draw_source(f, right_bot);
417
418 let footer_text = Paragraph::new(
419 "j/k step s/a step-over c/C prev/next CPI g/G first/last t tx picker q quit",
420 )
421 .block(Block::default().borders(Borders::ALL))
422 .alignment(Alignment::Center);
423 f.render_widget(footer_text, footer);
424 }
425
426 fn draw_stepper_header(&self, f: &mut Frame<'_>, area: Rect) {
427 let tx = self.current_tx();
428 let node = &tx.nodes[self.current_node];
429 let n_nodes = tx.nodes.len();
430 let step_total = node.steps.len();
431
432 let title_line = Line::from(vec![
434 Span::styled(
435 format!("{} ", tx.test_name),
436 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
437 ),
438 Span::raw(format!("· tx{} ", tx.tx_seq)),
439 Span::styled(
440 format!(
441 "· step {}/{} cu {}",
442 self.current_step + 1,
443 step_total.max(1),
444 node.steps
445 .get(self.current_step)
446 .map(|s| s.cu_cumulative)
447 .unwrap_or(0)
448 ),
449 Style::new().fg(Color::Yellow),
450 ),
451 ]);
452
453 let invocations_line = if n_nodes <= 1 {
457 Line::from(vec![Span::styled(
458 format!(
459 "invocations: 1/1 ({}) — single invocation, c/C disabled",
460 node.program_label
461 ),
462 Style::new().fg(Color::DarkGray),
463 )])
464 } else {
465 let mut spans: Vec<Span<'static>> = vec![Span::styled(
470 "invocations: ",
471 Style::new().fg(Color::DarkGray),
472 )];
473 for (i, n) in tx.nodes.iter().enumerate() {
474 let is_cur = i == self.current_node;
475 let kind = if i == 0 { "top" } else { "cpi" };
476 let label = format!(" #{} {} {} ", i + 1, kind, n.program_label);
477 let style = if is_cur {
478 Style::new().add_modifier(Modifier::REVERSED | Modifier::BOLD)
479 } else {
480 Style::new()
481 };
482 spans.push(Span::styled(label, style));
483 if i + 1 < tx.nodes.len() {
484 spans.push(Span::styled(" → ", Style::new().fg(Color::DarkGray)));
485 }
486 }
487 Line::from(spans)
488 };
489
490 let header = Paragraph::new(vec![title_line, invocations_line])
491 .block(Block::default().borders(Borders::ALL));
492 f.render_widget(header, area);
493 }
494
495 fn draw_instructions(&mut self, f: &mut Frame<'_>, area: Rect) {
496 let tx = self.current_tx();
497 let node = &tx.nodes[self.current_node];
498 let Some(step) = node.steps.get(self.current_step) else {
499 f.render_widget(
500 Paragraph::new("(no steps)").block(
501 Block::default()
502 .title(" instructions ")
503 .borders(Borders::ALL),
504 ),
505 area,
506 );
507 return;
508 };
509
510 if let Some(disasm) = self.session.programs.get(&node.program_id) {
515 self.draw_static_disasm(f, area, node, step, disasm);
516 return;
517 }
518
519 self.draw_trace_stream(f, area);
523 }
524
525 fn draw_static_disasm(
526 &self,
527 f: &mut Frame<'_>,
528 area: Rect,
529 node: &super::model::DebugNode,
530 step: &DebugStep,
531 disasm: &super::model::ProgramDisasm,
532 ) {
533 let center_idx = disasm
538 .pc_to_idx
539 .get(&step.pc)
540 .copied()
541 .or_else(|| {
542 disasm
543 .pc_to_idx
544 .range(..=step.pc)
545 .next_back()
546 .map(|(_, i)| *i)
547 })
548 .unwrap_or(0);
549
550 let window = area.height.saturating_sub(2) as usize;
551 let half = window / 2;
552 let start = center_idx.saturating_sub(half);
553 let end = (start + window).min(disasm.insns.len());
554
555 let mut rows: Vec<ListItem> = Vec::with_capacity(end - start);
556 for insn in &disasm.insns[start..end] {
557 if let Some(label) = &insn.func_label {
561 rows.push(ListItem::new(Line::from(vec![
562 Span::styled(" ", Style::new()),
563 Span::styled(
564 format!("┌── {label}"),
565 Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
566 ),
567 ])));
568 }
569
570 let is_current = insn.pc == step.pc;
571 let marker = if is_current { ">" } else { " " };
572 let mut spans = vec![
573 Span::raw(format!("{marker} ")),
574 Span::styled(
575 format!("pc {:>5} ", insn.pc),
576 Style::new().fg(Color::DarkGray),
577 ),
578 ];
579 spans.extend(insn.disasm_spans.iter().cloned());
580 let line = Line::from(spans);
581 let style = if is_current {
582 Style::new()
583 .bg(Color::DarkGray)
584 .add_modifier(Modifier::BOLD)
585 } else {
586 Style::new()
587 };
588 rows.push(ListItem::new(line).style(style));
589 }
590
591 let title = format!(
592 " {} · pc {} · trace step {}/{} ",
593 node.program_label,
594 step.pc,
595 self.current_step + 1,
596 node.steps.len()
597 );
598 let widget = List::new(rows).block(Block::default().title(title).borders(Borders::ALL));
599 f.render_widget(widget, area);
600 }
601
602 fn draw_trace_stream(&self, f: &mut Frame<'_>, area: Rect) {
604 let steps = self.current_steps();
605 let window = area.height.saturating_sub(2) as usize;
606 let half = window / 2;
607 let start = self.current_step.saturating_sub(half);
608 let end = (start + window).min(steps.len());
609
610 let items: Vec<ListItem> = steps[start..end]
611 .iter()
612 .enumerate()
613 .map(|(i, s)| {
614 let idx = start + i;
615 let marker = if idx == self.current_step { ">" } else { " " };
616 let mut spans = vec![
617 Span::raw(format!("{marker} ")),
618 Span::styled(format!("{:>6} ", idx), Style::new().fg(Color::DarkGray)),
619 Span::styled(format!("pc {:>5} ", s.pc), Style::new().fg(Color::DarkGray)),
620 ];
621 spans.extend(s.disasm_spans.iter().cloned());
622 let line = Line::from(spans);
623 let style = if idx == self.current_step {
624 Style::new()
625 .bg(Color::DarkGray)
626 .add_modifier(Modifier::BOLD)
627 } else {
628 Style::new()
629 };
630 ListItem::new(line).style(style)
631 })
632 .collect();
633
634 let title = format!(
635 " trace stream — no static disasm ({} / {}) ",
636 self.current_step + 1,
637 steps.len()
638 );
639 let widget = List::new(items).block(Block::default().title(title).borders(Borders::ALL));
640 f.render_widget(widget, area);
641 }
642
643 fn draw_registers(&self, f: &mut Frame<'_>, area: Rect) {
644 let Some(step) = self.current_steps().get(self.current_step) else {
645 let widget = Paragraph::new("(no steps)")
646 .block(Block::default().title(" registers ").borders(Borders::ALL));
647 f.render_widget(widget, area);
648 return;
649 };
650
651 let prev = self
652 .current_step
653 .checked_sub(1)
654 .and_then(|i| self.current_steps().get(i));
655
656 let mut lines = Vec::with_capacity(12);
657 for r in 0..11 {
658 let val = step.regs[r];
659 let changed = prev.is_some_and(|p| p.regs[r] != val);
660 let style = if changed {
661 Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
662 } else {
663 Style::new()
664 };
665 lines.push(Line::from(vec![
666 Span::raw(format!("r{:<2} ", r)),
667 Span::styled(format!("{:#018x}", val), style),
668 Span::styled(
669 format!(" ({})", val as i64),
670 Style::new().fg(Color::DarkGray),
671 ),
672 ]));
673 }
674 lines.push(Line::from(vec![
675 Span::raw("pc "),
676 Span::styled(
677 format!("{:#010x} ({})", step.pc, step.pc),
678 Style::new().fg(Color::Cyan),
679 ),
680 ]));
681
682 let widget = Paragraph::new(lines)
683 .block(Block::default().title(" registers ").borders(Borders::ALL));
684 f.render_widget(widget, area);
685 }
686
687 fn draw_source(&mut self, f: &mut Frame<'_>, area: Rect) {
688 let block = Block::default().title(" source ").borders(Borders::ALL);
689 let Some(step) = self.current_steps().get(self.current_step).cloned() else {
690 f.render_widget(Paragraph::new("(no steps)").block(block), area);
691 return;
692 };
693 let Some(loc) = step.src_loc.clone() else {
694 let node = &self.current_tx().nodes[self.current_node];
698 let program_disasm = self.session.programs.get(&node.program_id);
699 let msg = match program_disasm {
700 Some(d) if d.has_dwarf => format!(
701 "pc {pc:#x} has no DWARF line entry.\n\nThis is normal for hand-written asm \
702 entrypoints, inlined\nframes, compiler-generated stubs, and `.text` padding \
703 —\nLLVM only emits (file, line) tuples for Rust source.\nOther PCs in \
704 {program} resolve fine; stepping forward\nshould re-enter mapped code.\n\nIf \
705 you wrote an asm entrypoint, you can ignore this for\nthe asm region — \
706 registers + disasm above stay live.",
707 pc = step.pc,
708 program = node.program_label,
709 ),
710 Some(_) => format!(
711 "{program}: ELF has no DWARF line info.\n\nRebuild with debug info:\n \
712 CARGO_PROFILE_RELEASE_DEBUG=2 anchor build --no-idl\n\n(`anchor debugger` \
713 sets this for you when it rebuilds.\nThe flag only sticks if the .so we read \
714 came from that build.)",
715 program = node.program_label,
716 ),
717 None => format!(
718 "no static disasm or DWARF for {program} (pc {pc:#x}).\n\nThe program's \
719 deployed `.so` wasn't resolvable from the\nworkspace's Anchor.toml — \
720 third-party deploy, or\nmismatched program-id mapping.",
721 program = node.program_label,
722 pc = step.pc,
723 ),
724 };
725 f.render_widget(
726 Paragraph::new(msg).block(block).wrap(Wrap { trim: true }),
727 area,
728 );
729 return;
730 };
731
732 let resolved_path = resolve_src_path(
733 &loc.file,
734 &self.session.src_roots,
735 &self.session.path_rewrites,
736 loc.line,
737 );
738 let Some(path) = resolved_path else {
739 let msg = format!(
740 "can't read {}:{}\nno candidate path resolved\n\ntried roots: {}",
741 loc.file.display(),
742 loc.line,
743 self.session
744 .src_roots
745 .iter()
746 .map(|p| p.display().to_string())
747 .collect::<Vec<_>>()
748 .join(", ")
749 );
750 f.render_widget(
751 Paragraph::new(msg).block(block).wrap(Wrap { trim: true }),
752 area,
753 );
754 return;
755 };
756
757 let file_lines = match self.load_file(&path) {
762 Ok(lines) => lines,
763 Err(e) => {
764 let msg = format!("can't read {}:{}\n{e}", path.display(), loc.line);
765 f.render_widget(
766 Paragraph::new(msg).block(block).wrap(Wrap { trim: true }),
767 area,
768 );
769 return;
770 }
771 };
772 let lines = window_from_lines(file_lines, loc.line, area.height.saturating_sub(2) as u32);
773
774 let label = if let Some(cached) = self.label_cache.get(&path) {
779 cached.clone()
780 } else {
781 let l = classify(
782 &path,
783 &self.session.src_roots,
784 &self.session.path_rewrites,
785 self.session.cwd.as_deref(),
786 );
787 self.label_cache.insert(path.clone(), l.clone());
788 l
789 };
790 let title = format!(" {} · {}:{} ", label.label, label.path_display, loc.line);
791 let is_rust = loc
792 .file
793 .extension()
794 .and_then(|s| s.to_str())
795 .is_some_and(|e| e.eq_ignore_ascii_case("rs"));
796 let text: Vec<Line> = lines
797 .into_iter()
798 .map(|(n, content, is_current)| {
799 let mut spans = vec![Span::styled(
800 format!("{n:>5} "),
801 Style::new().fg(Color::DarkGray),
802 )];
803 if is_rust {
804 let key = (path.clone(), n);
809 let highlighted: Vec<Span<'static>> =
810 if let Some(cached) = self.highlight_cache.get(&key) {
811 cached.clone()
812 } else {
813 let h = highlight_rust(&content).spans;
814 self.highlight_cache.insert(key, h.clone());
815 h
816 };
817 if is_current {
818 for span in highlighted {
819 let mut style = span.style;
820 style.bg = Some(Color::DarkGray);
821 style = style.add_modifier(Modifier::BOLD);
822 spans.push(Span::styled(span.content.into_owned(), style));
823 }
824 } else {
825 spans.extend(highlighted);
826 }
827 } else {
828 let style = if is_current {
829 Style::new()
830 .bg(Color::DarkGray)
831 .add_modifier(Modifier::BOLD)
832 } else {
833 Style::new()
834 };
835 spans.push(Span::styled(content, style));
836 }
837 Line::from(spans)
838 })
839 .collect();
840
841 let widget =
842 Paragraph::new(text).block(Block::default().title(title).borders(Borders::ALL));
843 f.render_widget(widget, area);
844 }
845
846 fn handle_stepper_key(&mut self, k: KeyEvent) -> Flow {
847 let ctrl = k.modifiers.contains(KeyModifiers::CONTROL);
848 match k.code {
849 KeyCode::Char('q') => return Flow::Quit,
850 KeyCode::Char('t') | KeyCode::Esc => {
851 let row = self
855 .picker_rows
856 .iter()
857 .position(|r| matches!(r, PickerRow::Tx(i) if *i == self.current_tx));
858 self.picker.select(row);
859 self.screen = Screen::Picker;
860 }
861 KeyCode::Char(d @ '0'..='9') => {
862 self.key_buffer.push(d);
863 return Flow::Continue;
864 }
865 KeyCode::Char('j') | KeyCode::Down => self.repeat(|app| app.step_forward(1)),
866 KeyCode::Char('k') | KeyCode::Up => self.repeat(|app| app.step_back(1)),
867 KeyCode::Char('s') => self.repeat(App::step_over_forward),
868 KeyCode::Char('a') => self.repeat(App::step_over_back),
869 KeyCode::Char('g') => self.current_step = 0,
870 KeyCode::Char('G') => {
871 self.current_step = self.current_steps().len().saturating_sub(1);
872 }
873 KeyCode::Char('c') if !ctrl => {
874 self.current_node = self.current_node.saturating_sub(1);
875 self.current_step = 0;
876 }
877 KeyCode::Char('C') => {
878 let max = self.current_tx().nodes.len().saturating_sub(1);
879 self.current_node = (self.current_node + 1).min(max);
880 self.current_step = 0;
881 }
882 _ => {}
883 }
884 self.key_buffer.clear();
885 Flow::Continue
886 }
887
888 fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
889 let n = self
890 .key_buffer
891 .parse::<usize>()
892 .unwrap_or(1)
893 .clamp(1, 100_000);
894 for _ in 0..n {
895 f(self);
896 }
897 }
898
899 fn step_forward(&mut self, n: usize) {
900 let last = self.current_steps().len().saturating_sub(1);
901 self.current_step = (self.current_step + n).min(last);
902 }
903
904 fn step_back(&mut self, n: usize) {
905 self.current_step = self.current_step.saturating_sub(n);
906 }
907
908 fn step_over_forward(&mut self) {
911 let steps = self.current_steps();
912 let Some(cur) = steps.get(self.current_step) else {
913 return;
914 };
915 let base = cur.call_depth;
916 let start = self.current_step + 1;
917 let idx = steps[start..]
918 .iter()
919 .position(|s| s.call_depth <= base)
920 .map(|off| start + off)
921 .unwrap_or_else(|| steps.len().saturating_sub(1));
922 self.current_step = idx;
923 }
924
925 fn step_over_back(&mut self) {
926 let steps = self.current_steps();
927 let Some(cur) = steps.get(self.current_step) else {
928 return;
929 };
930 let base = cur.call_depth;
931 let idx = steps[..self.current_step]
932 .iter()
933 .rposition(|s| s.call_depth <= base)
934 .unwrap_or(0);
935 self.current_step = idx;
936 }
937}
938
939struct TerminalGuard {
940 term: DebugTerm,
941}
942
943impl TerminalGuard {
944 fn new(mut term: DebugTerm) -> Self {
945 let _ = enable_raw_mode();
946 let _ = execute!(term.backend_mut(), EnterAlternateScreen, EnableMouseCapture);
947 let _ = term.hide_cursor();
948 let _ = term.clear();
949 Self { term }
950 }
951}
952
953impl Drop for TerminalGuard {
954 fn drop(&mut self) {
955 let _ = disable_raw_mode();
956 let _ = execute!(
957 self.term.backend_mut(),
958 LeaveAlternateScreen,
959 DisableMouseCapture
960 );
961 let _ = self.term.show_cursor();
962 }
963}
964
965impl App {
966 fn load_file(&mut self, path: &std::path::Path) -> Result<&[String], String> {
971 if !self.file_cache.contains_key(path) {
972 let entry = match std::fs::read_to_string(path) {
973 Ok(s) => FileEntry::Loaded(s.lines().map(str::to_owned).collect()),
974 Err(e) => FileEntry::Missing(e.to_string()),
975 };
976 self.file_cache.insert(path.to_path_buf(), entry);
977 }
978 match self.file_cache.get(path).expect("just inserted") {
979 FileEntry::Loaded(lines) => Ok(lines.as_slice()),
980 FileEntry::Missing(msg) => Err(msg.clone()),
981 }
982 }
983}
984
985fn window_from_lines(lines: &[String], target_line: u32, height: u32) -> Vec<(u32, String, bool)> {
989 let target_idx = target_line.saturating_sub(1) as usize;
990 let half = (height / 2) as usize;
991 let start = target_idx.saturating_sub(half).min(lines.len());
992 let end = (start + height as usize).min(lines.len());
993 lines[start..end]
994 .iter()
995 .enumerate()
996 .map(|(i, l)| {
997 let n = (start + i + 1) as u32;
998 (n, l.clone(), n == target_line)
999 })
1000 .collect()
1001}
1002
1003fn resolve_src_path(
1015 file: &std::path::Path,
1016 roots: &[std::path::PathBuf],
1017 rewrites: &[(std::path::PathBuf, std::path::PathBuf)],
1018 line: u32,
1019) -> Option<std::path::PathBuf> {
1020 if file.is_absolute() && file.exists() {
1021 return Some(file.to_path_buf());
1022 }
1023
1024 if let Some(file_str) = file.to_str() {
1026 for (prefix, replacement) in rewrites {
1027 if let Some(prefix_str) = prefix.to_str() {
1028 if let Some(tail) = file_str.strip_prefix(prefix_str) {
1029 let candidate = replacement.join(tail.trim_start_matches('/'));
1030 if candidate.exists() {
1031 return Some(candidate);
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 let mut fallback: Option<std::path::PathBuf> = None;
1046 for root in roots {
1047 let candidate = root.join(file);
1048 if candidate.exists() {
1049 if line == 0 {
1050 return Some(candidate);
1051 }
1052 if let Ok(contents) = std::fs::read(&candidate) {
1053 let line_count = contents.iter().filter(|&&b| b == b'\n').count() + 1;
1054 if line as usize <= line_count {
1055 return Some(candidate);
1056 }
1057 }
1058 if fallback.is_none() {
1059 fallback = Some(candidate);
1060 }
1061 }
1062 }
1063
1064 if let Some(fb) = fallback {
1065 return Some(fb);
1066 }
1067
1068 if file.exists() {
1069 return Some(file.to_path_buf());
1070 }
1071 None
1072}