miden-debug 0.7.0

An interactive debugger for Miden VM programs
Documentation
use miden_assembly_syntax::diagnostics::Report;
use ratatui::{prelude::*, widgets::*};

use crate::{
    debug::{Breakpoint, BreakpointType},
    ui::{action::Action, panes::Pane, state::State, tui::Frame},
};

pub struct BreakpointsPane {
    focused: bool,
    focused_border_style: Style,
    breakpoint_selected: Option<u8>,
    breakpoints_hit: Vec<Breakpoint>,
    breakpoint_cycle: usize,
}

impl BreakpointsPane {
    pub fn new(focused: bool, focused_border_style: Style) -> Self {
        Self {
            focused,
            focused_border_style,
            breakpoint_selected: None,
            breakpoints_hit: vec![],
            breakpoint_cycle: 0,
        }
    }

    fn border_style(&self) -> Style {
        match self.focused {
            true => self.focused_border_style,
            false => Style::default(),
        }
    }

    fn border_type(&self) -> BorderType {
        match self.focused {
            true => BorderType::Thick,
            false => BorderType::Plain,
        }
    }
}

impl Pane for BreakpointsPane {
    fn height_constraint(&self) -> Constraint {
        match self.focused {
            true => Constraint::Fill(5),
            false => Constraint::Fill(5),
        }
    }

    fn init(&mut self, state: &State) -> Result<(), Report> {
        self.breakpoint_cycle = state.executor().cycle;
        self.breakpoints_hit.clear();
        self.breakpoint_selected = None;
        Ok(())
    }

    fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>, Report> {
        match action {
            Action::Focus => {
                self.focused = true;
            }
            Action::UnFocus => {
                self.focused = false;
            }
            Action::Down => {
                if let Some(prev) = self.breakpoint_selected.take() {
                    self.breakpoint_selected = state
                        .breakpoints
                        .iter()
                        .find_map(|bp| if bp.id > prev { Some(bp.id) } else { None })
                        .or_else(|| state.breakpoints.first().map(|bp| bp.id));
                } else {
                    self.breakpoint_selected = state.breakpoints.first().map(|bp| bp.id);
                }
                return Ok(Some(Action::Update));
            }
            Action::Up => {
                if let Some(prev) = self.breakpoint_selected.take() {
                    self.breakpoint_selected = state
                        .breakpoints
                        .iter()
                        .rev()
                        .find_map(|bp| if bp.id < prev { Some(bp.id) } else { None })
                        .or_else(|| state.breakpoints.last().map(|bp| bp.id));
                } else {
                    self.breakpoint_selected = state.breakpoints.last().map(|bp| bp.id);
                }
                return Ok(Some(Action::Update));
            }
            Action::Delete => {
                if let Some(prev) = self.breakpoint_selected.take() {
                    state.breakpoints.retain(|bp| bp.id != prev);
                    let select_next = state
                        .breakpoints
                        .iter()
                        .find_map(|bp| if bp.id > prev { Some(bp.id) } else { None })
                        .or_else(|| state.breakpoints.first().map(|bp| bp.id));
                    self.breakpoint_selected = select_next;
                }
            }
            Action::Reload => {
                self.init(state)?;
            }
            Action::Update => {
                if self.breakpoint_cycle < state.executor().cycle {
                    self.breakpoints_hit.clear();
                    self.breakpoints_hit.append(&mut state.breakpoints_hit);
                    if let Some(prev) = self.breakpoint_selected
                        && self.breakpoints_hit.iter().any(|bp| bp.id == prev && bp.is_one_shot())
                    {
                        self.breakpoint_selected = None;
                    }
                }
                self.breakpoint_cycle = state.executor().cycle;
            }
            _ => {}
        }

        Ok(None)
    }

    fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> {
        let mut breakpoints = self
            .breakpoints_hit
            .iter()
            .map(|bp| (true, bp))
            .chain(state.breakpoints.iter().filter_map(|bp| {
                if self.breakpoints_hit.iter().any(|hit| hit.id == bp.id) {
                    None
                } else {
                    Some((false, bp))
                }
            }))
            .filter(|(_, bp)| !bp.is_internal())
            .collect::<Vec<_>>();
        breakpoints.sort_by_key(|(_, bp)| bp.id);
        let user_created_breakpoints = breakpoints.len();
        let user_breakpoints_hit =
            self.breakpoints_hit.iter().filter(|bp| !bp.is_internal()).count();

        let fg = Color::Gray;
        let bg = Color::Black;
        let yellow = Color::Yellow;
        let gray = Color::Gray;
        let fg_hit = Color::Red;
        let bg_hit = Color::Black;
        let yellow_hit = Color::LightRed;
        let gray_hit = Color::DarkGray;
        let selected_index = if let Some(id) = self.breakpoint_selected {
            breakpoints.iter().position(|(_, bp)| bp.id == id)
        } else {
            None
        };
        let lines = breakpoints
            .into_iter()
            .map(|(is_hit, bp)| {
                let (_fg, bg, gray, yellow) = if is_hit {
                    (fg_hit, bg_hit, gray_hit, yellow_hit)
                } else {
                    (fg, bg, gray, yellow)
                };
                let yellow = Style::default().fg(yellow).bg(bg);
                let gray = Style::default().fg(gray).bg(bg);
                let gutter = if is_hit {
                    Span::styled("! ", Color::Red)
                } else {
                    Span::styled("", Style::default())
                };
                let line = match &bp.ty {
                    BreakpointType::Next
                    | BreakpointType::NextLine
                    | BreakpointType::Step
                    | BreakpointType::Finish => unreachable!(),
                    BreakpointType::StepN(n) => Line::from(vec![
                        gutter,
                        Span::styled("cycle:", yellow),
                        Span::styled(format!("{}", bp.creation_cycle + *n), gray),
                    ]),
                    BreakpointType::StepTo(cycle) => Line::from(vec![
                        gutter,
                        Span::styled("cycle:", yellow),
                        Span::styled(format!("{cycle}"), gray),
                    ]),
                    BreakpointType::File(pattern) => Line::from(vec![
                        gutter,
                        Span::styled("file:", yellow),
                        Span::styled(pattern.as_str(), gray),
                    ]),
                    BreakpointType::Line { pattern, line } => Line::from(vec![
                        gutter,
                        Span::styled("file:", yellow),
                        Span::styled(pattern.as_str(), gray),
                        Span::styled(format!(":{line}"), yellow),
                    ]),
                    BreakpointType::Called(pattern) => Line::from(vec![
                        gutter,
                        Span::styled("proc:", yellow),
                        Span::styled(pattern.as_str(), gray),
                    ]),
                    BreakpointType::Opcode(op) => Line::from(vec![
                        gutter,
                        Span::styled("opcode:", yellow),
                        Span::styled(format!("{op}"), gray),
                    ]),
                    BreakpointType::AsmOpcode(opcode_name) => Line::from(vec![
                        gutter,
                        Span::styled("masm op:", yellow),
                        Span::styled(*opcode_name, gray),
                    ]),
                    BreakpointType::Trace(id) => Line::from(vec![
                        gutter,
                        Span::styled("trace:", yellow),
                        Span::styled(format!("{id}"), gray),
                    ]),
                };
                if is_hit {
                    line.patch_style(Style::default().add_modifier(Modifier::BOLD))
                } else {
                    line
                }
            })
            .collect::<Vec<_>>();

        let list = List::new(lines)
            .block(Block::default().borders(Borders::ALL))
            .highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
            .highlight_spacing(HighlightSpacing::Always)
            .highlight_style(Style::default().add_modifier(Modifier::BOLD));
        let mut list_state = ListState::default().with_selected(selected_index);

        let pane = Block::default()
            .title("Breakpoints")
            .borders(Borders::ALL)
            .border_style(self.border_style())
            .border_type(self.border_type());
        let pane = if user_breakpoints_hit > 0 {
            pane.title_bottom(
                Line::styled(
                    format!(
                        " {user_breakpoints_hit} of {user_created_breakpoints} hit this cycle",
                    ),
                    Style::default().add_modifier(Modifier::ITALIC),
                )
                .right_aligned(),
            )
        } else {
            pane
        };

        frame.render_stateful_widget(list, area, &mut list_state);
        frame.render_widget(pane, area);
        Ok(())
    }
}