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