mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-17 00:47:12 +00:00
Add symbol mapping feature (#118)
This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: d1334bb79e)
Resolves #117
This commit is contained in:
@@ -1,28 +1,20 @@
|
||||
use egui::{Align, Layout, ScrollArea, Ui, Vec2};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use egui::{RichText, ScrollArea};
|
||||
use objdiff_core::{
|
||||
arch::ppc::ExceptionInfo,
|
||||
diff::ObjDiff,
|
||||
obj::{ObjInfo, ObjSymbol, SymbolRef},
|
||||
obj::{ObjInfo, ObjSymbol},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_strips},
|
||||
function_diff::FunctionDiffContext,
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
|
||||
View,
|
||||
},
|
||||
};
|
||||
|
||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == selected_symbol.symbol_name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||
let mut text = String::from("");
|
||||
|
||||
@@ -48,14 +40,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce
|
||||
}
|
||||
|
||||
fn extab_text_ui(
|
||||
ui: &mut Ui,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
) -> Option<()> {
|
||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
|
||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
||||
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||
let text = decode_extab(extab_entry);
|
||||
ui.colored_label(appearance.replace_color, &text);
|
||||
return Some(());
|
||||
@@ -65,137 +55,194 @@ fn extab_text_ui(
|
||||
}
|
||||
|
||||
fn extab_ui(
|
||||
ui: &mut Ui,
|
||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
_left: bool,
|
||||
_column: usize,
|
||||
) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
|
||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
||||
if let Some((_section, symbol)) =
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
{
|
||||
extab_text_ui(ui, ctx, symbol, appearance);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[must_use]
|
||||
pub fn extab_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.colored_label(appearance.highlight_color, name);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
format!("{:.0}%", match_percent.floor()),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user