diff --git a/objdiff-gui/src/hotkeys.rs b/objdiff-gui/src/hotkeys.rs index 1fab90ad..168965a1 100644 --- a/objdiff-gui/src/hotkeys.rs +++ b/objdiff-gui/src/hotkeys.rs @@ -118,3 +118,9 @@ const NEXT_DIFF_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CT pub fn consume_next_diff_shortcut(ctx: &Context) -> bool { ctx.input_mut(|i| i.consume_shortcut(&NEXT_DIFF_SHORTCUT)) } + +const GO_TO_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::G); + +pub fn consume_go_to_shortcut(ctx: &Context) -> bool { + ctx.input_mut(|i| i.consume_shortcut(&GO_TO_SHORTCUT)) +} diff --git a/objdiff-gui/src/views/diff.rs b/objdiff-gui/src/views/diff.rs index 76da8aa8..3fc3de68 100644 --- a/objdiff-gui/src/views/diff.rs +++ b/objdiff-gui/src/views/diff.rs @@ -23,7 +23,7 @@ use crate::{ column_layout::{render_header, render_strips, render_table}, data_diff::data_row_ui, extab_diff::extab_ui, - function_diff::{FunctionDiffContext, asm_col_ui}, + function_diff::{FunctionDiffContext, GoToTarget, GoToTargetType, asm_col_ui}, symbol_diff::{ DiffViewAction, DiffViewNavigation, DiffViewState, SymbolDiffContext, SymbolRefByName, View, match_color_for_symbol, symbol_context_menu_ui, symbol_hover_ui, symbol_list_ui, @@ -116,6 +116,57 @@ fn get_asm_text( asm_text } +fn try_scroll_to_go_to_target( + go_to_target: GoToTarget, + obj: &Object, + diff: &ObjectDiff, + symbol_idx: usize, +) -> Option { + match go_to_target { + GoToTarget::None => return None, + GoToTarget::LineNumber(target_line) => { + let symbol = obj.symbols.get(symbol_idx)?; + let section_index = symbol.section?; + let section = &obj.sections[section_index]; + for (ins_idx, ins_row) in diff.symbols[symbol_idx].instruction_rows.iter().enumerate() { + if let Some(ins_ref) = ins_row.ins_ref + && let Some(current_line) = + section.line_info.range(..=ins_ref.address).last().map(|(_, &b)| b) + && current_line == target_line + { + return Some(DiffViewAction::ScrollToRow(ins_idx)); + } + } + } + GoToTarget::Address(target_address) => { + let symbol = obj.symbols.get(symbol_idx)?; + let symbol_diff = diff.symbols.get(symbol_idx)?; + for (ins_idx, ins_row) in symbol_diff.instruction_rows.iter().enumerate() { + if let Some(ins_ref) = ins_row.ins_ref + && target_address == ins_ref.address.saturating_sub(symbol.address) + { + return Some(DiffViewAction::ScrollToRow(ins_idx)); + } + } + } + GoToTarget::VirtualAddress(target_virtual_address) => { + let symbol = obj.symbols.get(symbol_idx)?; + let section_index = symbol.section?; + let section = &obj.sections[section_index]; + let section_virtual_address = section.virtual_address?; + let target_offset = target_virtual_address.checked_sub(section_virtual_address)?; + for (ins_idx, ins_row) in diff.symbols[symbol_idx].instruction_rows.iter().enumerate() { + if let Some(ins_ref) = ins_row.ins_ref + && ins_ref.address == target_offset + { + return Some(DiffViewAction::ScrollToRow(ins_idx)); + } + } + } + }; + None +} + #[must_use] pub fn diff_view_ui( ui: &mut Ui, @@ -287,6 +338,44 @@ pub fn diff_view_ui( { ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); } + + if state.current_view == View::FunctionDiff { + ui.separator(); + + let mut goto_line_text = state.function_state.go_to_text_left.clone(); + let response = TextEdit::singleline(&mut goto_line_text) + .hint_text("Go to...") + .desired_width(100.0) + .ui(ui); + + let mut go_to_target_type_left: GoToTargetType = + state.function_state.go_to_target_type_left; + egui::ComboBox::from_id_salt("go_to_target_type_left") + .selected_text(go_to_target_type_left.to_string()) + .show_ui(ui, |ui| { + for go_to_target_type in [ + GoToTargetType::LineNumber, + GoToTargetType::Address, + GoToTargetType::VirtualAddress, + ] { + let response = ui.selectable_value( + &mut go_to_target_type_left, + go_to_target_type, + go_to_target_type.to_string(), + ); + if response.changed() { + ret = Some(DiffViewAction::SetGoToTargetType( + go_to_target_type_left, + false, + )); + } + } + }); + + if response.changed() { + ret = Some(DiffViewAction::SetGoToText(goto_line_text, false)); + } + } }); } else if left_ctx.status.success && !left_ctx.has_symbol() { ui.horizontal(|ui| { @@ -458,6 +547,48 @@ pub fn diff_view_ui( needs_separator = true; } + if state.current_view == View::FunctionDiff { + if needs_separator { + ui.separator(); + } + let mut goto_line_text = state.function_state.go_to_text_right.clone(); + let response = TextEdit::singleline(&mut goto_line_text) + .hint_text("Go to...") + .desired_width(100.0) + .ui(ui); + if hotkeys::consume_go_to_shortcut(ui.ctx()) { + response.request_focus(); + } + + let mut go_to_target_type_right: GoToTargetType = + state.function_state.go_to_target_type_right; + egui::ComboBox::from_id_salt("go_to_target_type_right") + .selected_text(go_to_target_type_right.to_string()) + .show_ui(ui, |ui| { + for go_to_target_type in [ + GoToTargetType::LineNumber, + GoToTargetType::Address, + GoToTargetType::VirtualAddress, + ] { + let response = ui.selectable_value( + &mut go_to_target_type_right, + go_to_target_type, + go_to_target_type.to_string(), + ); + if response.changed() { + ret = Some(DiffViewAction::SetGoToTargetType( + go_to_target_type_right, + true, + )); + } + } + }); + + if response.changed() { + ret = Some(DiffViewAction::SetGoToText(goto_line_text, true)); + } + } + if state.current_view == View::FunctionDiff || state.current_view == View::DataDiff { @@ -522,6 +653,18 @@ pub fn diff_view_ui( ui.label("Instruction count mismatch"); return; } + if let Some(action) = try_scroll_to_go_to_target( + state.function_state.go_to_target, + if state.function_state.go_to_target_is_right { right_obj } else { left_obj }, + if state.function_state.go_to_target_is_right { right_diff } else { left_diff }, + if state.function_state.go_to_target_is_right { + right_symbol_idx + } else { + left_symbol_idx + }, + ) { + ret = Some(action); + } let instructions_len = left_symbol_diff.instruction_rows.len(); let mut min_row = None; let mut max_row = None; @@ -799,6 +942,14 @@ fn diff_col_ui( }, ); } else { + if let Some(action) = try_scroll_to_go_to_target( + state.function_state.go_to_target, + obj, + diff, + symbol_idx, + ) { + ret = Some(action); + } render_table( ui, available_width / 2.0, diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index b00377f5..1f662259 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -20,10 +20,43 @@ use crate::views::{ symbol_diff::DiffViewAction, }; +#[derive(Debug, Default, Copy, Clone)] +pub enum GoToTarget { + #[default] + None, + LineNumber(u32), + Address(u64), + VirtualAddress(u64), +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub enum GoToTargetType { + #[default] + LineNumber, + Address, + VirtualAddress, +} + +impl core::fmt::Display for GoToTargetType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + GoToTargetType::LineNumber => write!(f, "Line Number"), + GoToTargetType::Address => write!(f, "Address / Offset"), + GoToTargetType::VirtualAddress => write!(f, "Virtual Address"), + } + } +} + #[derive(Default)] pub struct FunctionViewState { left_highlight: HighlightKind, right_highlight: HighlightKind, + pub go_to_target: GoToTarget, + pub go_to_target_is_right: bool, + pub go_to_text_left: String, + pub go_to_target_type_left: GoToTargetType, + pub go_to_text_right: String, + pub go_to_target_type_right: GoToTargetType, } impl FunctionViewState { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 0260fd79..86233457 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -24,7 +24,7 @@ use crate::{ views::{ appearance::Appearance, diff::{context_menu_items_ui, hover_items_ui}, - function_diff::FunctionViewState, + function_diff::{FunctionViewState, GoToTarget, GoToTargetType}, write_text, }, }; @@ -83,6 +83,12 @@ pub enum DiffViewAction { SetShowDataFlow(bool), // Scrolls a row of the function view table into view. ScrollToRow(usize), + /// Changes the text of the "Go to..." field. + /// The bool is true for right, false for left. + SetGoToText(String, bool), + /// Changes the dropdown next to the "Go to..." field. + /// The bool is true for right, false for left. + SetGoToTargetType(GoToTargetType, bool), } #[derive(Debug, Clone, Default, Eq, PartialEq)] @@ -204,6 +210,7 @@ impl DiffViewState { // Clear the scroll flags to prevent it from scrolling continuously. self.symbol_state.autoscroll_to_highlighted_symbols = false; self.scroll_to_diff_row = None; + self.function_state.go_to_target = GoToTarget::None; let Some(action) = action else { return; @@ -369,6 +376,23 @@ impl DiffViewState { DiffViewAction::ScrollToRow(row) => { self.scroll_to_diff_row = Some(row); } + DiffViewAction::SetGoToText(text, is_right) => { + self.function_state.go_to_target_is_right = is_right; + if is_right { + self.function_state.go_to_text_right = text; + } else { + self.function_state.go_to_text_left = text; + } + self.resolve_go_to_target(is_right); + } + DiffViewAction::SetGoToTargetType(target_type, is_right) => { + if is_right { + self.function_state.go_to_target_type_right = target_type; + } else { + self.function_state.go_to_target_type_left = target_type; + } + self.resolve_go_to_target(is_right); + } } } @@ -418,6 +442,36 @@ impl DiffViewState { self.search_regex = search_regex; } } + + fn resolve_go_to_target(&mut self, is_right: bool) { + let target_type = if is_right { + self.function_state.go_to_target_type_right + } else { + self.function_state.go_to_target_type_left + }; + let target_text = if is_right { + &self.function_state.go_to_text_right + } else { + &self.function_state.go_to_text_left + }; + match target_type { + GoToTargetType::LineNumber => { + if let Ok(line_num) = target_text.trim().parse::() { + self.function_state.go_to_target = GoToTarget::LineNumber(line_num) + } + } + GoToTargetType::Address => { + if let Ok(address) = u64::from_str_radix(target_text.trim(), 16) { + self.function_state.go_to_target = GoToTarget::Address(address) + } + } + GoToTargetType::VirtualAddress => { + if let Ok(virtual_address) = u64::from_str_radix(target_text.trim(), 16) { + self.function_state.go_to_target = GoToTarget::VirtualAddress(virtual_address) + } + } + } + } } struct ResolvedSymbol<'obj> {