From 8c55754fe6f46bbc8a0ee4984129b65a4706478e Mon Sep 17 00:00:00 2001 From: LagoLunatic Date: Tue, 30 Dec 2025 17:34:40 -0500 Subject: [PATCH 1/2] Function diff: Implement "Go to line number" --- objdiff-gui/src/hotkeys.rs | 6 ++++ objdiff-gui/src/views/diff.rs | 49 ++++++++++++++++++++++++++ objdiff-gui/src/views/function_diff.rs | 2 ++ objdiff-gui/src/views/symbol_diff.rs | 9 +++++ 4 files changed, 66 insertions(+) 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..b0975a1c 100644 --- a/objdiff-gui/src/views/diff.rs +++ b/objdiff-gui/src/views/diff.rs @@ -116,6 +116,28 @@ fn get_asm_text( asm_text } +fn try_scroll_to_line_number( + scroll_to_line_number: Option, + obj: &Object, + diff: &ObjectDiff, + symbol_idx: usize, +) -> Option { + let target_line = scroll_to_line_number?; + 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)); + } + } + None +} + #[must_use] pub fn diff_view_ui( ui: &mut Ui, @@ -464,6 +486,17 @@ pub fn diff_view_ui( if needs_separator { ui.separator(); } + let mut goto_line_text = state.function_state.go_to_line_text.clone(); + let response = TextEdit::singleline(&mut goto_line_text) + .hint_text("Go to line number") + .desired_width(100.0) + .ui(ui); + if hotkeys::consume_go_to_shortcut(ui.ctx()) { + response.request_focus(); + } + if response.changed() { + ret = Some(DiffViewAction::SetGoToText(goto_line_text)); + } if ui .button("⏴ Prev diff") .on_hover_text_at_pointer("Scroll to the previous difference (Ctrl+Up)") @@ -522,6 +555,14 @@ pub fn diff_view_ui( ui.label("Instruction count mismatch"); return; } + if let Some(action) = try_scroll_to_line_number( + state.function_state.scroll_to_line_number, + right_obj, + right_diff, + right_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 +840,14 @@ fn diff_col_ui( }, ); } else { + if let Some(action) = try_scroll_to_line_number( + state.function_state.scroll_to_line_number, + 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..6923aa81 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -24,6 +24,8 @@ use crate::views::{ pub struct FunctionViewState { left_highlight: HighlightKind, right_highlight: HighlightKind, + pub scroll_to_line_number: Option, + pub go_to_line_text: String, } impl FunctionViewState { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 0260fd79..dc279845 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -83,6 +83,8 @@ pub enum DiffViewAction { SetShowDataFlow(bool), // Scrolls a row of the function view table into view. ScrollToRow(usize), + /// Sets the text of the line number jump field and try to scroll that line into view. + SetGoToText(String), } #[derive(Debug, Clone, Default, Eq, PartialEq)] @@ -204,6 +206,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.scroll_to_line_number = None; let Some(action) = action else { return; @@ -369,6 +372,12 @@ impl DiffViewState { DiffViewAction::ScrollToRow(row) => { self.scroll_to_diff_row = Some(row); } + DiffViewAction::SetGoToText(text) => { + if let Ok(line_num) = text.trim().parse::() { + self.function_state.scroll_to_line_number = Some(line_num); + } + self.function_state.go_to_line_text = text; + } } } From 33c149fb183689cbe9d99125350d8ec36e3285e3 Mon Sep 17 00:00:00 2001 From: LagoLunatic Date: Wed, 17 Jun 2026 20:17:42 -0400 Subject: [PATCH 2/2] Add dropdown to goto offset/address and split left/right --- objdiff-gui/src/views/diff.rs | 158 ++++++++++++++++++++----- objdiff-gui/src/views/function_diff.rs | 35 +++++- objdiff-gui/src/views/symbol_diff.rs | 61 ++++++++-- 3 files changed, 216 insertions(+), 38 deletions(-) diff --git a/objdiff-gui/src/views/diff.rs b/objdiff-gui/src/views/diff.rs index b0975a1c..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,25 +116,54 @@ fn get_asm_text( asm_text } -fn try_scroll_to_line_number( - scroll_to_line_number: Option, +fn try_scroll_to_go_to_target( + go_to_target: GoToTarget, obj: &Object, diff: &ObjectDiff, symbol_idx: usize, ) -> Option { - let target_line = scroll_to_line_number?; - 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)); + 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 } @@ -309,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| { @@ -480,22 +547,53 @@ pub fn diff_view_ui( needs_separator = true; } - if state.current_view == View::FunctionDiff - || state.current_view == View::DataDiff - { + if state.current_view == View::FunctionDiff { if needs_separator { ui.separator(); } - let mut goto_line_text = state.function_state.go_to_line_text.clone(); + 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 line number") + .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)); + ret = Some(DiffViewAction::SetGoToText(goto_line_text, true)); + } + } + + if state.current_view == View::FunctionDiff + || state.current_view == View::DataDiff + { + if needs_separator { + ui.separator(); } if ui .button("⏴ Prev diff") @@ -555,11 +653,15 @@ pub fn diff_view_ui( ui.label("Instruction count mismatch"); return; } - if let Some(action) = try_scroll_to_line_number( - state.function_state.scroll_to_line_number, - right_obj, - right_diff, - right_symbol_idx, + 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); } @@ -840,8 +942,8 @@ fn diff_col_ui( }, ); } else { - if let Some(action) = try_scroll_to_line_number( - state.function_state.scroll_to_line_number, + if let Some(action) = try_scroll_to_go_to_target( + state.function_state.go_to_target, obj, diff, symbol_idx, diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index 6923aa81..1f662259 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -20,12 +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 scroll_to_line_number: Option, - pub go_to_line_text: String, + 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 dc279845..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,8 +83,12 @@ pub enum DiffViewAction { SetShowDataFlow(bool), // Scrolls a row of the function view table into view. ScrollToRow(usize), - /// Sets the text of the line number jump field and try to scroll that line into view. - SetGoToText(String), + /// 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)] @@ -206,7 +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.scroll_to_line_number = None; + self.function_state.go_to_target = GoToTarget::None; let Some(action) = action else { return; @@ -372,11 +376,22 @@ impl DiffViewState { DiffViewAction::ScrollToRow(row) => { self.scroll_to_diff_row = Some(row); } - DiffViewAction::SetGoToText(text) => { - if let Ok(line_num) = text.trim().parse::() { - self.function_state.scroll_to_line_number = Some(line_num); + 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.function_state.go_to_line_text = 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); } } } @@ -427,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> {