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, pub mnemonic: SharedString, pub operands: SharedString, pub comment: Option, pub source: Option, } /// How a piece of an operand reads. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Piece { Register, Number, Mark, } static PIECES: LazyLock = LazyLock::new(|| { Regex::new( r"(?x) (?P\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\#?-?(?:0x[0-9a-fA-F]+|\b[0-9]+\b)) |(?P[\[\],+*!\-\#])", ) .expect("the operand pattern compiles") }); /// Registers, numbers and marks in `operands`, in order. pub(crate) fn pieces(operands: &str) -> Vec<(Range, 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>, current: Option, breakpoints: Vec, on_breakpoint: Option, } impl Disassembly { pub fn new(id: impl Into, instructions: impl Into>>) -> 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) -> 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> = 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, 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 = 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!("{: 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) ] ); } }