Files
Ely-GPUI-Components/src/debug/disasm.rs
T
ZacharyZhang-NY 2f3b0ac8bb T20 round 1 fixes
- the hex demo's selection stops at memory's end
- a disassembly gutter fills its row, so an empty one takes a press
- a breakpoint's box keeps its press from the row
- ruler ticks keep the places their step needs
- the toolbar names each usual key for the platform; the app binds them
- tests: gutter press, box press, tick places
2026-09-26 02:54:23 -04:00

304 lines
11 KiB
Rust

use std::{ops::Range, rc::Rc, sync::LazyLock};
use gpui::{
App, ElementId, HighlightStyle, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div,
prelude::*, uniform_list,
};
use regex::Regex;
use super::OnIndex;
use crate::{
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize, TextSize},
};
/// An instruction: its address, its bytes, its mnemonic and operands, an optional comment, and the source line it starts, when known.
#[derive(Clone, Debug, PartialEq)]
pub struct Instruction {
pub address: u64,
pub bytes: Vec<u8>,
pub mnemonic: SharedString,
pub operands: SharedString,
pub comment: Option<SharedString>,
pub source: Option<SharedString>,
}
/// How a piece of an operand reads.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Piece {
Register,
Number,
Mark,
}
static PIECES: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
(?P<register>\b(?:[re]?(?:ax|bx|cx|dx|si|di|sp|bp|ip)|r(?:[89]|1[0-5])[dwb]?|[xyz]mm[0-9]{1,2}|[abcd][lh]|[xw](?:[12]?[0-9]|30)|sp|lr|fp|pc|[xw]zr|[vqdsbh][0-9]{1,2})\b)
|(?P<number>\#?-?(?:0x[0-9a-fA-F]+|\b[0-9]+\b))
|(?P<mark>[\[\],+*!\-\#])",
)
.expect("the operand pattern compiles")
});
/// Registers, numbers and marks in `operands`, in order.
pub(crate) fn pieces(operands: &str) -> Vec<(Range<usize>, Piece)> {
PIECES
.captures_iter(operands)
.map(|found| {
let (kind, hit) = if let Some(hit) = found.name("register") {
(Piece::Register, hit)
} else if let Some(hit) = found.name("number") {
(Piece::Number, hit)
} else {
(
Piece::Mark,
found.name("mark").expect("a piece is one of three"),
)
};
(hit.range(), kind)
})
.collect()
}
/// A row: an instruction, or the source line above one.
#[derive(Clone, Copy)]
enum Line {
Source(usize),
Code(usize),
}
/// Machine code as read: address, bytes, the mnemonic and its operands colored, comments, and the source lines they came from. The current instruction is marked; a press in the gutter sets or clears a breakpoint. Long listings draw only the rows in view; it fills its box.
#[derive(IntoElement)]
pub struct Disassembly {
id: ElementId,
instructions: Rc<Vec<Instruction>>,
current: Option<usize>,
breakpoints: Vec<usize>,
on_breakpoint: Option<OnIndex>,
}
impl Disassembly {
pub fn new(id: impl Into<ElementId>, instructions: impl Into<Rc<Vec<Instruction>>>) -> Self {
Self {
id: id.into(),
instructions: instructions.into(),
current: None,
breakpoints: Vec::new(),
on_breakpoint: None,
}
}
pub fn current(mut self, instruction: usize) -> Self {
self.current = Some(instruction);
self
}
pub fn breakpoints(mut self, instructions: impl IntoIterator<Item = usize>) -> Self {
self.breakpoints = instructions.into_iter().collect();
self
}
pub fn on_breakpoint(
mut self,
handler: impl Fn(usize, &mut Window, &mut App) + 'static,
) -> Self {
self.on_breakpoint = Some(Rc::new(handler));
self
}
}
impl RenderOnce for Disassembly {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let lines: Rc<Vec<Line>> = Rc::new(
self.instructions
.iter()
.enumerate()
.flat_map(|(ix, instruction)| {
instruction
.source
.as_ref()
.map(|_| Line::Source(ix))
.into_iter()
.chain([Line::Code(ix)])
})
.collect(),
);
let widest = self
.instructions
.iter()
.map(|instruction| instruction.bytes.len())
.max()
.unwrap_or(0);
let (instructions, current, breakpoints, on_breakpoint, id) = (
self.instructions.clone(),
self.current,
self.breakpoints.clone(),
self.on_breakpoint.clone(),
self.id.clone(),
);
let list = uniform_list(
(self.id.clone(), "rows"),
lines.len(),
move |range, _, cx| {
let colors = cx.theme().colors.clone();
let syntax = colors.syntax;
range
.map(|row| match lines[row] {
Line::Source(ix) => div()
.id(row)
.w_full()
.pl_8()
.whitespace_nowrap()
.text_color(syntax.comment)
.child(
instructions[ix]
.source
.clone()
.expect("a source row has its line"),
),
Line::Code(ix) => {
let instruction = &instructions[ix];
let styles: Vec<(Range<usize>, HighlightStyle)> =
pieces(&instruction.operands)
.into_iter()
.map(|(range, piece)| {
let color = match piece {
Piece::Register => syntax.variable,
Piece::Number => syntax.number,
Piece::Mark => syntax.punctuation,
};
(range, color.into())
})
.collect();
let (marked, is_current) =
(breakpoints.contains(&ix), current == Some(ix));
let toggle = on_breakpoint.clone();
let hex: Vec<String> = instruction
.bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
div()
.id(row)
.w_full()
.flex()
.gap_3()
.whitespace_nowrap()
.when(is_current, |line| line.bg(colors.warning.opacity(0.12)))
.child(
div()
.id((id.clone(), format!("gutter-{ix}")))
.flex_none()
.w_5()
.flex()
.items_center()
.justify_center()
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default()
})
.when_some(toggle, |gutter, toggle| {
gutter.on_click(move |_, window, cx| {
toggle(ix, window, cx)
})
})
.map(|gutter| {
if is_current {
gutter.child(
Icon::new(IconName::ArrowRight)
.size(IconSize::Xs)
.color(colors.warning),
)
} else if marked {
gutter.child(
div().size_2().rounded_full().bg(colors.danger),
)
} else {
gutter
}
}),
)
.child(
div()
.flex_none()
.text_color(colors.fg_subtle)
.child(format!("{:#010x}", instruction.address)),
)
.child(div().flex_none().text_color(colors.fg_subtle).child(
format!("{:<width$}", hex.join(" "), width = widest * 3),
))
.child(
div()
.flex_none()
.w_12()
.text_color(syntax.keyword)
.child(instruction.mnemonic.clone()),
)
.child(
StyledText::new(instruction.operands.clone())
.with_highlights(styles),
)
.children(instruction.comment.clone().map(|comment| {
div()
.text_color(syntax.comment)
.child(format!("; {comment}"))
}))
}
})
.collect()
},
)
.size_full();
div()
.size_full()
.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg)
.child(list)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(operands: &str) -> Vec<(&str, Piece)> {
pieces(operands)
.into_iter()
.map(|(range, piece)| (&operands[range], piece))
.collect()
}
#[test]
fn operands_split_into_registers_numbers_and_marks() {
use Piece::*;
assert_eq!(
kinds("qword ptr [rbp - 0x18], rax"),
[
("[", Mark),
("rbp", Register),
("-", Mark),
("0x18", Number),
("]", Mark),
(",", Mark),
("rax", Register)
]
);
assert_eq!(
kinds("x0, [sp, #16]"),
[
("x0", Register),
(",", Mark),
("[", Mark),
("sp", Register),
(",", Mark),
("#16", Number),
("]", Mark)
]
);
}
}