mirror of https://github.com/encounter/objdiff.git
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:
parent
603dbd6882
commit
741d93e211
|
@ -395,6 +395,15 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bimap"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -2852,7 +2861,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-cli"
|
name = "objdiff-cli"
|
||||||
version = "2.2.2"
|
version = "2.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argp",
|
"argp",
|
||||||
|
@ -2874,10 +2883,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.2.2"
|
version = "2.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arm-attr",
|
"arm-attr",
|
||||||
|
"bimap",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
|
@ -2913,7 +2923,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-gui"
|
name = "objdiff-gui"
|
||||||
version = "2.2.2"
|
version = "2.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
@ -13,7 +13,7 @@ strip = "debuginfo"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.2.2"
|
version = "2.3.0"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
|
@ -133,6 +133,13 @@
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ref": "#/$defs/metadata"
|
"ref": "#/$defs/metadata"
|
||||||
|
},
|
||||||
|
"symbol_mappings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Manual symbol mappings from target to base.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
let unit_path =
|
let unit_path =
|
||||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||||
|
|
||||||
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
|
let Some(object) = project_config
|
||||||
if obj.name.as_deref() == Some(u) {
|
.units
|
||||||
|
.as_deref_mut()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter_mut()
|
||||||
|
.find_map(|obj| {
|
||||||
|
if obj.name.as_deref() == Some(u) {
|
||||||
|
resolve_paths(obj);
|
||||||
|
return Some(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
let up = unit_path.as_deref()?;
|
||||||
|
|
||||||
resolve_paths(obj);
|
resolve_paths(obj);
|
||||||
return Some(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
let up = unit_path.as_deref()?;
|
if [&obj.base_path, &obj.target_path]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||||
|
.any(|p| p == up)
|
||||||
|
{
|
||||||
|
return Some(obj);
|
||||||
|
}
|
||||||
|
|
||||||
resolve_paths(obj);
|
None
|
||||||
|
})
|
||||||
if [&obj.base_path, &obj.target_path]
|
else {
|
||||||
.into_iter()
|
|
||||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
|
||||||
.any(|p| p == up)
|
|
||||||
{
|
|
||||||
return Some(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}) else {
|
|
||||||
bail!("Unit not found: {}", u)
|
bail!("Unit not found: {}", u)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
} else if let Some(symbol_name) = &args.symbol {
|
} else if let Some(symbol_name) = &args.symbol {
|
||||||
let mut idx = None;
|
let mut idx = None;
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
for (i, obj) in project_config
|
||||||
|
.units
|
||||||
|
.as_deref_mut()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
resolve_paths(obj);
|
resolve_paths(obj);
|
||||||
|
|
||||||
if obj
|
if obj
|
||||||
|
@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
}
|
}
|
||||||
match (count, idx) {
|
match (count, idx) {
|
||||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||||
(1, Some(i)) => &mut project_config.objects[i],
|
(1, Some(i)) => &mut project_config.units_mut()[i],
|
||||||
(2.., Some(_)) => bail!(
|
(2.., Some(_)) => bail!(
|
||||||
"Multiple instances of {} were found, try specifying a unit",
|
"Multiple instances of {} were found, try specifying a unit",
|
||||||
symbol_name
|
symbol_name
|
||||||
|
@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
struct FunctionDiffUi {
|
struct FunctionDiffUi {
|
||||||
relax_reloc_diffs: bool,
|
relax_reloc_diffs: bool,
|
||||||
left_highlight: HighlightKind,
|
left_highlight: HighlightKind,
|
||||||
|
@ -758,7 +770,7 @@ impl FunctionDiffUi {
|
||||||
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn print_sym(
|
fn print_sym(
|
||||||
&self,
|
&self,
|
||||||
out: &mut Text<'static>,
|
out: &mut Text<'static>,
|
||||||
|
|
|
@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
};
|
};
|
||||||
info!(
|
info!(
|
||||||
"Generating report for {} units (using {} threads)",
|
"Generating report for {} units (using {} threads)",
|
||||||
project.objects.len(),
|
project.units().len(),
|
||||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||||
if args.deduplicate {
|
if args.deduplicate {
|
||||||
// If deduplicating, we need to run single-threaded
|
// If deduplicating, we need to run single-threaded
|
||||||
for object in &mut project.objects {
|
for object in project.units.as_deref_mut().unwrap_or_default() {
|
||||||
if let Some(unit) = report_object(
|
if let Some(unit) = report_object(
|
||||||
object,
|
object,
|
||||||
project_dir,
|
project_dir,
|
||||||
|
@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let vec = project
|
let vec = project
|
||||||
.objects
|
.units
|
||||||
|
.as_deref_mut()
|
||||||
|
.unwrap_or_default()
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.map(|object| {
|
.map(|object| {
|
||||||
report_object(
|
report_object(
|
||||||
|
@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
}
|
}
|
||||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||||
let mut categories = Vec::new();
|
let mut categories = Vec::new();
|
||||||
for category in &project.progress_categories {
|
for category in project.progress_categories() {
|
||||||
categories.push(ReportCategory {
|
categories.push(ReportCategory {
|
||||||
id: category.id.clone(),
|
id: category.id.clone(),
|
||||||
name: category.name.clone(),
|
name: category.name.clone(),
|
||||||
|
|
|
@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
|
||||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
|
||||||
dwarf = ["gimli"]
|
dwarf = ["gimli"]
|
||||||
mips = ["any-arch", "rabbitizer"]
|
mips = ["any-arch", "rabbitizer"]
|
||||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||||
|
@ -32,6 +32,7 @@ features = ["all"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
flagset = "0.4"
|
flagset = "0.4"
|
||||||
|
|
|
@ -1,77 +1,100 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
fs,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufReader, Read},
|
io::{BufReader, BufWriter, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use bimap::BiBTreeMap;
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
|
|
||||||
#[inline]
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
fn bool_true() -> bool { true }
|
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
|
||||||
pub struct ProjectConfig {
|
pub struct ProjectConfig {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub min_version: Option<String>,
|
pub min_version: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub custom_make: Option<String>,
|
pub custom_make: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub custom_args: Option<Vec<String>>,
|
pub custom_args: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub target_dir: Option<PathBuf>,
|
pub target_dir: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub base_dir: Option<PathBuf>,
|
pub base_dir: Option<PathBuf>,
|
||||||
#[serde(default = "bool_true")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_base: bool,
|
pub build_base: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_target: bool,
|
pub build_target: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub watch_patterns: Option<Vec<Glob>>,
|
pub watch_patterns: Option<Vec<Glob>>,
|
||||||
#[serde(default, alias = "units")]
|
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||||
pub objects: Vec<ProjectObject>,
|
pub units: Option<Vec<ProjectObject>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
impl ProjectConfig {
|
||||||
|
#[inline]
|
||||||
|
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
||||||
|
self.units.get_or_insert_with(Vec::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||||
|
self.progress_categories.as_deref().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||||
|
self.progress_categories.get_or_insert_with(Vec::new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ProjectObject {
|
pub struct ProjectObject {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub target_path: Option<PathBuf>,
|
pub target_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub base_path: Option<PathBuf>,
|
pub base_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[deprecated(note = "Use metadata.complete")]
|
#[deprecated(note = "Use metadata.complete")]
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub scratch: Option<ScratchConfig>,
|
pub scratch: Option<ScratchConfig>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metadata: Option<ProjectObjectMetadata>,
|
pub metadata: Option<ProjectObjectMetadata>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub symbol_mappings: Option<SymbolMappings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
pub type SymbolMappings = BiBTreeMap<String, String>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ProjectObjectMetadata {
|
pub struct ProjectObjectMetadata {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub source_path: Option<String>,
|
pub source_path: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub progress_categories: Option<Vec<String>>,
|
pub progress_categories: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_generated: Option<bool>,
|
pub auto_generated: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ProjectProgressCategory {
|
pub struct ProjectProgressCategory {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -112,12 +135,12 @@ impl ProjectObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&self) -> Option<bool> {
|
pub fn complete(&self) -> Option<bool> {
|
||||||
#[allow(deprecated)]
|
#[expect(deprecated)]
|
||||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||||
#[allow(deprecated)]
|
#[expect(deprecated)]
|
||||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,16 +155,16 @@ impl ProjectObject {
|
||||||
|
|
||||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ScratchConfig {
|
pub struct ScratchConfig {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub platform: Option<String>,
|
pub platform: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub compiler: Option<String>,
|
pub compiler: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub c_flags: Option<String>,
|
pub c_flags: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub ctx_path: Option<PathBuf>,
|
pub ctx_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_ctx: bool,
|
pub build_ctx: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||||
|
@ -154,7 +177,7 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct ProjectConfigInfo {
|
pub struct ProjectConfigInfo {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub timestamp: FileTime,
|
pub timestamp: Option<FileTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||||
|
@ -180,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||||
result = Err(e);
|
result = Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
|
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_project_config(
|
||||||
|
config: &ProjectConfig,
|
||||||
|
info: &ProjectConfigInfo,
|
||||||
|
) -> Result<ProjectConfigInfo> {
|
||||||
|
if let Some(last_ts) = info.timestamp {
|
||||||
|
// Check if the file has changed since we last read it
|
||||||
|
if let Ok(metadata) = fs::metadata(&info.path) {
|
||||||
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
if ts != last_ts {
|
||||||
|
return Err(anyhow!("Config file has changed since last read"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut writer =
|
||||||
|
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
|
||||||
|
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||||
|
match ext {
|
||||||
|
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||||
|
"yml" | "yaml" => {
|
||||||
|
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||||
|
}?;
|
||||||
|
let file = writer.into_inner().context("Failed to flush file")?;
|
||||||
|
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||||
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve_branches(&mut diff);
|
resolve_branches(&mut diff);
|
||||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_code(
|
pub fn diff_code(
|
||||||
|
@ -67,7 +67,7 @@ pub fn diff_code(
|
||||||
right.arg_diff = result.right_args_diff;
|
right.arg_diff = result.right_args_diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = left_out.insts.len();
|
let total = left_out.insts.len().max(right_out.insts.len());
|
||||||
let percent = if diff_state.diff_count >= total {
|
let percent = if diff_state.diff_count >= total {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
|
@ -77,13 +77,13 @@ pub fn diff_code(
|
||||||
Ok((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: left_diff,
|
instructions: left_diff,
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: right_diff,
|
instructions: right_diff,
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
|
@ -211,7 +211,7 @@ fn arg_eq(
|
||||||
left_diff: &ObjInsDiff,
|
left_diff: &ObjInsDiff,
|
||||||
right_diff: &ObjInsDiff,
|
right_diff: &ObjInsDiff,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
return match left {
|
match left {
|
||||||
ObjInsArg::PlainText(l) => match right {
|
ObjInsArg::PlainText(l) => match right {
|
||||||
ObjInsArg::PlainText(r) => l == r,
|
ObjInsArg::PlainText(r) => l == r,
|
||||||
_ => false,
|
_ => false,
|
||||||
|
@ -236,7 +236,7 @@ fn arg_eq(
|
||||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
|
@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
|
||||||
Ok((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||||
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
|
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare the data sections of two object files.
|
/// Compare the data sections of two object files.
|
||||||
|
@ -158,13 +158,13 @@ pub fn diff_data_symbol(
|
||||||
Ok((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(match_percent),
|
match_percent: Some(match_percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(match_percent),
|
match_percent: Some(match_percent),
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub enum DiffText<'a> {
|
||||||
Eol,
|
Eol,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub enum HighlightKind {
|
pub enum HighlightKind {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::collections::HashSet;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::SymbolMappings,
|
||||||
diff::{
|
diff::{
|
||||||
code::{diff_code, no_diff_code, process_code_symbol},
|
code::{diff_code, no_diff_code, process_code_symbol},
|
||||||
data::{
|
data::{
|
||||||
|
@ -161,6 +162,8 @@ pub struct DiffObjConfig {
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub space_between_args: bool,
|
pub space_between_args: bool,
|
||||||
pub combine_data_sections: bool,
|
pub combine_data_sections: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub symbol_mappings: MappingConfig,
|
||||||
// x86
|
// x86
|
||||||
pub x86_formatter: X86Formatter,
|
pub x86_formatter: X86Formatter,
|
||||||
// MIPS
|
// MIPS
|
||||||
|
@ -182,6 +185,7 @@ impl Default for DiffObjConfig {
|
||||||
relax_reloc_diffs: false,
|
relax_reloc_diffs: false,
|
||||||
space_between_args: true,
|
space_between_args: true,
|
||||||
combine_data_sections: false,
|
combine_data_sections: false,
|
||||||
|
symbol_mappings: Default::default(),
|
||||||
x86_formatter: Default::default(),
|
x86_formatter: Default::default(),
|
||||||
mips_abi: Default::default(),
|
mips_abi: Default::default(),
|
||||||
mips_instr_category: Default::default(),
|
mips_instr_category: Default::default(),
|
||||||
|
@ -223,8 +227,10 @@ impl ObjSectionDiff {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ObjSymbolDiff {
|
pub struct ObjSymbolDiff {
|
||||||
|
/// The symbol ref this object
|
||||||
pub symbol_ref: SymbolRef,
|
pub symbol_ref: SymbolRef,
|
||||||
pub diff_symbol: Option<SymbolRef>,
|
/// The symbol ref in the _other_ object that this symbol was diffed against
|
||||||
|
pub target_symbol: Option<SymbolRef>,
|
||||||
pub instructions: Vec<ObjInsDiff>,
|
pub instructions: Vec<ObjInsDiff>,
|
||||||
pub match_percent: Option<f32>,
|
pub match_percent: Option<f32>,
|
||||||
}
|
}
|
||||||
|
@ -294,8 +300,13 @@ pub struct ObjInsBranchTo {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ObjDiff {
|
pub struct ObjDiff {
|
||||||
|
/// A list of all section diffs in the object.
|
||||||
pub sections: Vec<ObjSectionDiff>,
|
pub sections: Vec<ObjSectionDiff>,
|
||||||
|
/// Common BSS symbols don't live in a section, so they're stored separately.
|
||||||
pub common: Vec<ObjSymbolDiff>,
|
pub common: Vec<ObjSymbolDiff>,
|
||||||
|
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
|
||||||
|
/// that are being mapped to the other object.
|
||||||
|
pub mapping_symbols: Vec<ObjSymbolDiff>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjDiff {
|
impl ObjDiff {
|
||||||
|
@ -303,13 +314,14 @@ impl ObjDiff {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
sections: Vec::with_capacity(obj.sections.len()),
|
sections: Vec::with_capacity(obj.sections.len()),
|
||||||
common: Vec::with_capacity(obj.common.len()),
|
common: Vec::with_capacity(obj.common.len()),
|
||||||
|
mapping_symbols: vec![],
|
||||||
};
|
};
|
||||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||||
let mut symbols = Vec::with_capacity(section.symbols.len());
|
let mut symbols = Vec::with_capacity(section.symbols.len());
|
||||||
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
||||||
symbols.push(ObjSymbolDiff {
|
symbols.push(ObjSymbolDiff {
|
||||||
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
||||||
diff_symbol: None,
|
target_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
});
|
});
|
||||||
|
@ -328,7 +340,7 @@ impl ObjDiff {
|
||||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||||
result.common.push(ObjSymbolDiff {
|
result.common.push(ObjSymbolDiff {
|
||||||
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
||||||
diff_symbol: None,
|
target_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
});
|
});
|
||||||
|
@ -378,7 +390,7 @@ pub fn diff_objs(
|
||||||
right: Option<&ObjInfo>,
|
right: Option<&ObjInfo>,
|
||||||
prev: Option<&ObjInfo>,
|
prev: Option<&ObjInfo>,
|
||||||
) -> Result<DiffObjsResult> {
|
) -> Result<DiffObjsResult> {
|
||||||
let symbol_matches = matching_symbols(left, right, prev)?;
|
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
|
||||||
let section_matches = matching_sections(left, right)?;
|
let section_matches = matching_sections(left, right)?;
|
||||||
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||||
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||||
|
@ -529,6 +541,17 @@ pub fn diff_objs(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
|
||||||
|
(right.as_mut(), left.as_mut())
|
||||||
|
{
|
||||||
|
if let Some(right_name) = &config.symbol_mappings.selecting_left {
|
||||||
|
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
|
||||||
|
}
|
||||||
|
if let Some(left_name) = &config.symbol_mappings.selecting_right {
|
||||||
|
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(DiffObjsResult {
|
Ok(DiffObjsResult {
|
||||||
left: left.map(|(_, o)| o),
|
left: left.map(|(_, o)| o),
|
||||||
right: right.map(|(_, o)| o),
|
right: right.map(|(_, o)| o),
|
||||||
|
@ -536,6 +559,63 @@ pub fn diff_objs(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
|
||||||
|
/// symbols in the other object that match the selected symbol's section and kind. This allows
|
||||||
|
/// us to display match percentages for all symbols in the other object that could be selected.
|
||||||
|
fn generate_mapping_symbols(
|
||||||
|
base_obj: &ObjInfo,
|
||||||
|
base_name: &str,
|
||||||
|
target_obj: &ObjInfo,
|
||||||
|
target_out: &mut ObjDiff,
|
||||||
|
config: &DiffObjConfig,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||||
|
let Some(base_section) = base_section else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let base_code = match base_section.kind {
|
||||||
|
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
for (target_section_index, target_section) in
|
||||||
|
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||||
|
{
|
||||||
|
for (target_symbol_index, _target_symbol) in
|
||||||
|
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
|
||||||
|
{
|
||||||
|
let target_symbol_ref =
|
||||||
|
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||||
|
match base_section.kind {
|
||||||
|
ObjSectionKind::Code => {
|
||||||
|
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||||
|
let (left_diff, _right_diff) = diff_code(
|
||||||
|
&target_code,
|
||||||
|
base_code.as_ref().unwrap(),
|
||||||
|
target_symbol_ref,
|
||||||
|
base_symbol_ref,
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
ObjSectionKind::Data => {
|
||||||
|
let (left_diff, _right_diff) =
|
||||||
|
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
ObjSectionKind::Bss => {
|
||||||
|
let (left_diff, _right_diff) =
|
||||||
|
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
struct SymbolMatch {
|
struct SymbolMatch {
|
||||||
left: Option<SymbolRef>,
|
left: Option<SymbolRef>,
|
||||||
|
@ -551,19 +631,115 @@ struct SectionMatch {
|
||||||
section_kind: ObjSectionKind,
|
section_kind: ObjSectionKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct MappingConfig {
|
||||||
|
/// Manual symbol mappings
|
||||||
|
pub mappings: SymbolMappings,
|
||||||
|
/// The right object symbol name that we're selecting a left symbol for
|
||||||
|
pub selecting_left: Option<String>,
|
||||||
|
/// The left object symbol name that we're selecting a right symbol for
|
||||||
|
pub selecting_right: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||||
|
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||||
|
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||||
|
if symbol.name == name {
|
||||||
|
return Some(SymbolRef { section_idx, symbol_idx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_symbol_mappings(
|
||||||
|
left: &ObjInfo,
|
||||||
|
right: &ObjInfo,
|
||||||
|
mapping_config: &MappingConfig,
|
||||||
|
left_used: &mut HashSet<SymbolRef>,
|
||||||
|
right_used: &mut HashSet<SymbolRef>,
|
||||||
|
matches: &mut Vec<SymbolMatch>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// If we're selecting a symbol to use as a comparison, mark it as used
|
||||||
|
// This ensures that we don't match it to another symbol at any point
|
||||||
|
if let Some(left_name) = &mapping_config.selecting_left {
|
||||||
|
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
|
||||||
|
left_used.insert(left_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(right_name) = &mapping_config.selecting_right {
|
||||||
|
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
|
||||||
|
right_used.insert(right_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply manual symbol mappings
|
||||||
|
for (left_name, right_name) in &mapping_config.mappings {
|
||||||
|
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if left_used.contains(&left_symbol) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if right_used.contains(&right_symbol) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let left_section = &left.sections[left_symbol.section_idx];
|
||||||
|
let right_section = &right.sections[right_symbol.section_idx];
|
||||||
|
if left_section.kind != right_section.kind {
|
||||||
|
log::warn!(
|
||||||
|
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
|
||||||
|
left_name,
|
||||||
|
left_section.kind,
|
||||||
|
right_name,
|
||||||
|
right_section.kind
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matches.push(SymbolMatch {
|
||||||
|
left: Some(left_symbol),
|
||||||
|
right: Some(right_symbol),
|
||||||
|
prev: None, // TODO
|
||||||
|
section_kind: left_section.kind,
|
||||||
|
});
|
||||||
|
left_used.insert(left_symbol);
|
||||||
|
right_used.insert(right_symbol);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Find matching symbols between each object.
|
/// Find matching symbols between each object.
|
||||||
fn matching_symbols(
|
fn matching_symbols(
|
||||||
left: Option<&ObjInfo>,
|
left: Option<&ObjInfo>,
|
||||||
right: Option<&ObjInfo>,
|
right: Option<&ObjInfo>,
|
||||||
prev: Option<&ObjInfo>,
|
prev: Option<&ObjInfo>,
|
||||||
|
mappings: &MappingConfig,
|
||||||
) -> Result<Vec<SymbolMatch>> {
|
) -> Result<Vec<SymbolMatch>> {
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
|
let mut left_used = HashSet::new();
|
||||||
let mut right_used = HashSet::new();
|
let mut right_used = HashSet::new();
|
||||||
if let Some(left) = left {
|
if let Some(left) = left {
|
||||||
|
if let Some(right) = right {
|
||||||
|
apply_symbol_mappings(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
mappings,
|
||||||
|
&mut left_used,
|
||||||
|
&mut right_used,
|
||||||
|
&mut matches,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||||
|
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||||
|
if left_used.contains(&symbol_ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let symbol_match = SymbolMatch {
|
let symbol_match = SymbolMatch {
|
||||||
left: Some(SymbolRef { section_idx, symbol_idx }),
|
left: Some(symbol_ref),
|
||||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||||
prev: find_symbol(prev, symbol, section, None),
|
prev: find_symbol(prev, symbol, section, None),
|
||||||
section_kind: section.kind,
|
section_kind: section.kind,
|
||||||
|
@ -575,8 +751,12 @@ fn matching_symbols(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||||
|
let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx };
|
||||||
|
if left_used.contains(&symbol_ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let symbol_match = SymbolMatch {
|
let symbol_match = SymbolMatch {
|
||||||
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }),
|
left: Some(symbol_ref),
|
||||||
right: find_common_symbol(right, symbol),
|
right: find_common_symbol(right, symbol),
|
||||||
prev: find_common_symbol(prev, symbol),
|
prev: find_common_symbol(prev, symbol),
|
||||||
section_kind: ObjSectionKind::Bss,
|
section_kind: ObjSectionKind::Bss,
|
||||||
|
|
|
@ -112,6 +112,15 @@ pub struct ObjIns {
|
||||||
pub orig: Option<String>,
|
pub orig: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||||
|
pub enum ObjSymbolKind {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Function,
|
||||||
|
Object,
|
||||||
|
Section,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ObjSymbol {
|
pub struct ObjSymbol {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -120,6 +129,7 @@ pub struct ObjSymbol {
|
||||||
pub section_address: u64,
|
pub section_address: u64,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub size_known: bool,
|
pub size_known: bool,
|
||||||
|
pub kind: ObjSymbolKind,
|
||||||
pub flags: ObjSymbolFlagSet,
|
pub flags: ObjSymbolFlagSet,
|
||||||
pub addend: i64,
|
pub addend: i64,
|
||||||
/// Original virtual address (from .note.split section)
|
/// Original virtual address (from .note.split section)
|
||||||
|
|
|
@ -23,6 +23,7 @@ use crate::{
|
||||||
obj::{
|
obj::{
|
||||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||||
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
|
ObjSymbolKind,
|
||||||
},
|
},
|
||||||
util::{read_u16, read_u32},
|
util::{read_u16, read_u32},
|
||||||
};
|
};
|
||||||
|
@ -94,6 +95,13 @@ fn to_obj_symbol(
|
||||||
})
|
})
|
||||||
.unwrap_or(&[]);
|
.unwrap_or(&[]);
|
||||||
|
|
||||||
|
let kind = match symbol.kind() {
|
||||||
|
SymbolKind::Text => ObjSymbolKind::Function,
|
||||||
|
SymbolKind::Data => ObjSymbolKind::Object,
|
||||||
|
SymbolKind::Section => ObjSymbolKind::Section,
|
||||||
|
_ => ObjSymbolKind::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(ObjSymbol {
|
Ok(ObjSymbol {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
demangled_name,
|
demangled_name,
|
||||||
|
@ -101,6 +109,7 @@ fn to_obj_symbol(
|
||||||
section_address,
|
section_address,
|
||||||
size: symbol.size(),
|
size: symbol.size(),
|
||||||
size_known: symbol.size() != 0,
|
size_known: symbol.size() != 0,
|
||||||
|
kind,
|
||||||
flags,
|
flags,
|
||||||
addend,
|
addend,
|
||||||
virtual_address,
|
virtual_address,
|
||||||
|
@ -173,12 +182,19 @@ fn symbols_by_section(
|
||||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||||
let mut iter = result.iter_mut().peekable();
|
let mut iter = result.iter_mut().peekable();
|
||||||
while let Some(symbol) = iter.next() {
|
while let Some(symbol) = iter.next() {
|
||||||
if symbol.size == 0 {
|
if symbol.kind == ObjSymbolKind::Unknown && symbol.size == 0 {
|
||||||
if let Some(next_symbol) = iter.peek() {
|
if let Some(next_symbol) = iter.peek() {
|
||||||
symbol.size = next_symbol.address - symbol.address;
|
symbol.size = next_symbol.address - symbol.address;
|
||||||
} else {
|
} else {
|
||||||
symbol.size = (section.address + section.size) - symbol.address;
|
symbol.size = (section.address + section.size) - symbol.address;
|
||||||
}
|
}
|
||||||
|
// Set symbol kind if we ended up with a non-zero size
|
||||||
|
if symbol.size > 0 {
|
||||||
|
symbol.kind = match section.kind {
|
||||||
|
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||||
|
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result.is_empty() {
|
if result.is_empty() {
|
||||||
|
@ -196,6 +212,10 @@ fn symbols_by_section(
|
||||||
section_address: 0,
|
section_address: 0,
|
||||||
size: section.size,
|
size: section.size,
|
||||||
size_known: true,
|
size_known: true,
|
||||||
|
kind: match section.kind {
|
||||||
|
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||||
|
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||||
|
},
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
addend: 0,
|
addend: 0,
|
||||||
virtual_address: None,
|
virtual_address: None,
|
||||||
|
@ -281,6 +301,7 @@ fn find_section_symbol(
|
||||||
section_address: 0,
|
section_address: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
size_known: false,
|
size_known: false,
|
||||||
|
kind: ObjSymbolKind::Section,
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
addend: address as i64 - section.address() as i64,
|
addend: address as i64 - section.address() as i64,
|
||||||
virtual_address: None,
|
virtual_address: None,
|
||||||
|
@ -568,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||||
size: symbol.size,
|
size: symbol.size,
|
||||||
size_known: symbol.size_known,
|
size_known: symbol.size_known,
|
||||||
|
kind: symbol.kind,
|
||||||
flags: symbol.flags,
|
flags: symbol.flags,
|
||||||
addend: symbol.addend,
|
addend: symbol.addend,
|
||||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||||
|
|
|
@ -15,7 +15,8 @@ use globset::{Glob, GlobSet};
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
config::{
|
config::{
|
||||||
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS,
|
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
|
||||||
|
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||||
},
|
},
|
||||||
diff::DiffObjConfig,
|
diff::DiffObjConfig,
|
||||||
};
|
};
|
||||||
|
@ -42,7 +43,7 @@ use crate::{
|
||||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||||
jobs::{jobs_menu_ui, jobs_window},
|
jobs::{jobs_menu_ui, jobs_window},
|
||||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ impl Default for ViewState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configuration for a single object file.
|
/// The configuration for a single object file.
|
||||||
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ObjectConfig {
|
pub struct ObjectConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub target_path: Option<PathBuf>,
|
pub target_path: Option<PathBuf>,
|
||||||
|
@ -98,6 +99,23 @@ pub struct ObjectConfig {
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
pub scratch: Option<ScratchConfig>,
|
pub scratch: Option<ScratchConfig>,
|
||||||
pub source_path: Option<String>,
|
pub source_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub symbol_mappings: SymbolMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ProjectObject> for ObjectConfig {
|
||||||
|
fn from(object: &ProjectObject) -> Self {
|
||||||
|
Self {
|
||||||
|
name: object.name().to_string(),
|
||||||
|
target_path: object.target_path.clone(),
|
||||||
|
base_path: object.base_path.clone(),
|
||||||
|
reverse_fn_order: object.reverse_fn_order(),
|
||||||
|
complete: object.complete(),
|
||||||
|
scratch: object.scratch.clone(),
|
||||||
|
source_path: object.source_path().cloned(),
|
||||||
|
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -117,8 +135,14 @@ pub struct AppState {
|
||||||
pub obj_change: bool,
|
pub obj_change: bool,
|
||||||
pub queue_build: bool,
|
pub queue_build: bool,
|
||||||
pub queue_reload: bool,
|
pub queue_reload: bool,
|
||||||
|
pub current_project_config: Option<ProjectConfig>,
|
||||||
pub project_config_info: Option<ProjectConfigInfo>,
|
pub project_config_info: Option<ProjectConfigInfo>,
|
||||||
pub last_mod_check: Instant,
|
pub last_mod_check: Instant,
|
||||||
|
/// The right object symbol name that we're selecting a left symbol for
|
||||||
|
pub selecting_left: Option<String>,
|
||||||
|
/// The left object symbol name that we're selecting a right symbol for
|
||||||
|
pub selecting_right: Option<String>,
|
||||||
|
pub config_error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
|
@ -132,8 +156,12 @@ impl Default for AppState {
|
||||||
obj_change: false,
|
obj_change: false,
|
||||||
queue_build: false,
|
queue_build: false,
|
||||||
queue_reload: false,
|
queue_reload: false,
|
||||||
|
current_project_config: None,
|
||||||
project_config_info: None,
|
project_config_info: None,
|
||||||
last_mod_check: Instant::now(),
|
last_mod_check: Instant::now(),
|
||||||
|
selecting_left: None,
|
||||||
|
selecting_right: None,
|
||||||
|
config_error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,7 +242,10 @@ impl AppState {
|
||||||
self.config_change = true;
|
self.config_change = true;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.current_project_config = None;
|
||||||
self.project_config_info = None;
|
self.project_config_info = None;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||||
|
@ -222,6 +253,8 @@ impl AppState {
|
||||||
self.config.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||||
|
@ -229,12 +262,122 @@ impl AppState {
|
||||||
self.config.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
|
||||||
self.config.selected_obj = Some(object);
|
if self.config.selected_obj.as_ref().is_some_and(|existing| existing == &config) {
|
||||||
|
// Don't reload the object if there were no changes
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.config.selected_obj = Some(config);
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_selected_obj(&mut self) {
|
||||||
|
self.config.selected_obj = None;
|
||||||
|
self.obj_change = true;
|
||||||
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selecting_left(&mut self, right: &str) {
|
||||||
|
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
object.symbol_mappings.remove_by_right(right);
|
||||||
|
self.selecting_left = Some(right.to_string());
|
||||||
|
self.queue_reload = true;
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selecting_right(&mut self, left: &str) {
|
||||||
|
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
object.symbol_mappings.remove_by_left(left);
|
||||||
|
self.selecting_right = Some(left.to_string());
|
||||||
|
self.queue_reload = true;
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
|
||||||
|
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||||
|
log::warn!("No selected object");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
|
if left == right {
|
||||||
|
object.symbol_mappings.remove_by_left(&left);
|
||||||
|
object.symbol_mappings.remove_by_right(&right);
|
||||||
|
} else {
|
||||||
|
object.symbol_mappings.insert(left.clone(), right.clone());
|
||||||
|
}
|
||||||
|
self.queue_reload = true;
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_selection(&mut self) {
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
|
self.queue_reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_mappings(&mut self) {
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
|
if let Some(object) = self.config.selected_obj.as_mut() {
|
||||||
|
object.symbol_mappings.clear();
|
||||||
|
}
|
||||||
|
self.queue_reload = true;
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_selecting_symbol(&self) -> bool {
|
||||||
|
self.selecting_left.is_some() || self.selecting_right.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(&mut self) {
|
||||||
|
let (Some(config), Some(info)) =
|
||||||
|
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Update the project config with the current state
|
||||||
|
if let Some(object) = self.config.selected_obj.as_ref() {
|
||||||
|
if let Some(existing) = config.units.as_mut().and_then(|v| {
|
||||||
|
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||||
|
}) {
|
||||||
|
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(object.symbol_mappings.clone())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(existing) =
|
||||||
|
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||||
|
{
|
||||||
|
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(object.symbol_mappings.clone())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save the updated project config
|
||||||
|
match save_project_config(config, info) {
|
||||||
|
Ok(new_info) => *info = new_info,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to save project config: {e}");
|
||||||
|
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,31 +516,41 @@ impl App {
|
||||||
debug_assert!(jobs.results.is_empty());
|
debug_assert!(jobs.results.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_update(&mut self, ctx: &egui::Context) {
|
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
|
||||||
self.appearance.post_update(ctx);
|
self.appearance.post_update(ctx);
|
||||||
|
|
||||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||||
config_state.post_update(ctx, jobs, &self.state);
|
config_state.post_update(ctx, jobs, &self.state);
|
||||||
diff_state.post_update(ctx, jobs, &self.state);
|
diff_state.post_update(action, ctx, jobs, &self.state);
|
||||||
|
|
||||||
let Ok(mut state) = self.state.write() else {
|
let Ok(mut state) = self.state.write() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let state = &mut *state;
|
let state = &mut *state;
|
||||||
|
|
||||||
if let Some(info) = &state.project_config_info {
|
let mut mod_check = false;
|
||||||
if file_modified(&info.path, info.timestamp) {
|
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||||
state.config_change = true;
|
state.last_mod_check = Instant::now();
|
||||||
|
mod_check = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mod_check {
|
||||||
|
if let Some(info) = &state.project_config_info {
|
||||||
|
if let Some(last_ts) = info.timestamp {
|
||||||
|
if file_modified(&info.path, last_ts) {
|
||||||
|
state.config_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.config_change {
|
if state.config_change {
|
||||||
state.config_change = false;
|
state.config_change = false;
|
||||||
match load_project_config(state) {
|
match load_project_config(state) {
|
||||||
Ok(()) => config_state.load_error = None,
|
Ok(()) => state.config_error = None,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load project config: {e}");
|
log::error!("Failed to load project config: {e}");
|
||||||
config_state.load_error = Some(format!("{e}"));
|
state.config_error = Some(format!("{e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -432,8 +585,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(result) = &diff_state.build {
|
if let Some(result) = &diff_state.build {
|
||||||
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
if mod_check {
|
||||||
state.last_mod_check = Instant::now();
|
|
||||||
if let Some((obj, _)) = &result.first_obj {
|
if let Some((obj, _)) = &result.first_obj {
|
||||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||||
if file_modified(path, timestamp) {
|
if file_modified(path, timestamp) {
|
||||||
|
@ -457,11 +609,11 @@ impl App {
|
||||||
&& state.config.selected_obj.is_some()
|
&& state.config.selected_obj.is_some()
|
||||||
&& !jobs.is_running(Job::ObjDiff)
|
&& !jobs.is_running(Job::ObjDiff)
|
||||||
{
|
{
|
||||||
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config)));
|
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
|
||||||
state.queue_build = false;
|
state.queue_build = false;
|
||||||
state.queue_reload = false;
|
state.queue_reload = false;
|
||||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||||
let mut diff_config = ObjDiffConfig::from_config(&state.config);
|
let mut diff_config = ObjDiffConfig::from_state(state);
|
||||||
// Don't build, just reload the current files
|
// Don't build, just reload the current files
|
||||||
diff_config.build_base = false;
|
diff_config.build_base = false;
|
||||||
diff_config.build_target = false;
|
diff_config.build_target = false;
|
||||||
|
@ -636,6 +788,11 @@ impl eframe::App for App {
|
||||||
{
|
{
|
||||||
state.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
|
if ui.button("Clear custom symbol mappings").clicked() {
|
||||||
|
state.clear_mappings();
|
||||||
|
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||||
|
state.queue_reload = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if jobs_menu_ui(ui, jobs, appearance) {
|
if jobs_menu_ui(ui, jobs, appearance) {
|
||||||
|
@ -652,17 +809,18 @@ impl eframe::App for App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||||
if diff_state.current_view == View::FunctionDiff && build_success {
|
action = if diff_state.current_view == View::FunctionDiff && build_success {
|
||||||
function_diff_ui(ui, diff_state, appearance);
|
function_diff_ui(ui, diff_state, appearance)
|
||||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||||
data_diff_ui(ui, diff_state, appearance);
|
data_diff_ui(ui, diff_state, appearance)
|
||||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||||
extab_diff_ui(ui, diff_state, appearance);
|
extab_diff_ui(ui, diff_state, appearance)
|
||||||
} else {
|
} else {
|
||||||
symbol_diff_ui(ui, diff_state, appearance);
|
symbol_diff_ui(ui, diff_state, appearance)
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
project_window(ctx, state, show_project_config, config_state, appearance);
|
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||||
|
@ -674,10 +832,10 @@ impl eframe::App for App {
|
||||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||||
jobs_window(ctx, show_jobs, jobs, appearance);
|
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||||
|
|
||||||
self.post_update(ctx);
|
self.post_update(ctx, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the frame work to save state before shutdown.
|
/// Called by the framework to save state before shutdown.
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
if let Ok(state) = self.state.read() {
|
if let Ok(state) = self.state.read() {
|
||||||
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||||
|
|
|
@ -2,6 +2,10 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use eframe::Storage;
|
use eframe::Storage;
|
||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
|
use objdiff_core::{
|
||||||
|
config::ScratchConfig,
|
||||||
|
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
||||||
|
|
||||||
|
@ -11,7 +15,7 @@ pub struct AppConfigVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfigVersion {
|
impl Default for AppConfigVersion {
|
||||||
fn default() -> Self { Self { version: 1 } }
|
fn default() -> Self { Self { version: 2 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
||||||
|
@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
|
||||||
let str = storage.get_string(CONFIG_KEY)?;
|
let str = storage.get_string(CONFIG_KEY)?;
|
||||||
match ron::from_str::<AppConfigVersion>(&str) {
|
match ron::from_str::<AppConfigVersion>(&str) {
|
||||||
Ok(version) => match version.version {
|
Ok(version) => match version.version {
|
||||||
1 => from_str::<AppConfig>(&str),
|
2 => from_str::<AppConfig>(&str),
|
||||||
|
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("Unknown config version: {}", version.version);
|
log::warn!("Unknown config version: {}", version.version);
|
||||||
None
|
None
|
||||||
|
@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ScratchConfigV1 {
|
||||||
|
#[serde(default)]
|
||||||
|
pub platform: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compiler: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub c_flags: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ctx_path: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_ctx: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScratchConfigV1 {
|
||||||
|
fn into_config(self) -> ScratchConfig {
|
||||||
|
ScratchConfig {
|
||||||
|
platform: self.platform,
|
||||||
|
compiler: self.compiler,
|
||||||
|
c_flags: self.c_flags,
|
||||||
|
ctx_path: self.ctx_path,
|
||||||
|
build_ctx: self.build_ctx.then_some(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ObjectConfigV1 {
|
||||||
|
pub name: String,
|
||||||
|
pub target_path: Option<PathBuf>,
|
||||||
|
pub base_path: Option<PathBuf>,
|
||||||
|
pub reverse_fn_order: Option<bool>,
|
||||||
|
pub complete: Option<bool>,
|
||||||
|
pub scratch: Option<ScratchConfigV1>,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectConfigV1 {
|
||||||
|
fn into_config(self) -> ObjectConfig {
|
||||||
|
ObjectConfig {
|
||||||
|
name: self.name,
|
||||||
|
target_path: self.target_path,
|
||||||
|
base_path: self.base_path,
|
||||||
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
|
complete: self.complete,
|
||||||
|
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||||
|
source_path: self.source_path,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DiffObjConfigV1 {
|
||||||
|
pub relax_reloc_diffs: bool,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub space_between_args: bool,
|
||||||
|
pub combine_data_sections: bool,
|
||||||
|
// x86
|
||||||
|
pub x86_formatter: X86Formatter,
|
||||||
|
// MIPS
|
||||||
|
pub mips_abi: MipsAbi,
|
||||||
|
pub mips_instr_category: MipsInstrCategory,
|
||||||
|
// ARM
|
||||||
|
pub arm_arch_version: ArmArchVersion,
|
||||||
|
pub arm_unified_syntax: bool,
|
||||||
|
pub arm_av_registers: bool,
|
||||||
|
pub arm_r9_usage: ArmR9Usage,
|
||||||
|
pub arm_sl_usage: bool,
|
||||||
|
pub arm_fp_usage: bool,
|
||||||
|
pub arm_ip_usage: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DiffObjConfigV1 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
relax_reloc_diffs: false,
|
||||||
|
space_between_args: true,
|
||||||
|
combine_data_sections: false,
|
||||||
|
x86_formatter: Default::default(),
|
||||||
|
mips_abi: Default::default(),
|
||||||
|
mips_instr_category: Default::default(),
|
||||||
|
arm_arch_version: Default::default(),
|
||||||
|
arm_unified_syntax: true,
|
||||||
|
arm_av_registers: false,
|
||||||
|
arm_r9_usage: Default::default(),
|
||||||
|
arm_sl_usage: false,
|
||||||
|
arm_fp_usage: false,
|
||||||
|
arm_ip_usage: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiffObjConfigV1 {
|
||||||
|
fn into_config(self) -> DiffObjConfig {
|
||||||
|
DiffObjConfig {
|
||||||
|
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||||
|
space_between_args: self.space_between_args,
|
||||||
|
combine_data_sections: self.combine_data_sections,
|
||||||
|
x86_formatter: self.x86_formatter,
|
||||||
|
mips_abi: self.mips_abi,
|
||||||
|
mips_instr_category: self.mips_instr_category,
|
||||||
|
arm_arch_version: self.arm_arch_version,
|
||||||
|
arm_unified_syntax: self.arm_unified_syntax,
|
||||||
|
arm_av_registers: self.arm_av_registers,
|
||||||
|
arm_r9_usage: self.arm_r9_usage,
|
||||||
|
arm_sl_usage: self.arm_sl_usage,
|
||||||
|
arm_fp_usage: self.arm_fp_usage,
|
||||||
|
arm_ip_usage: self.arm_ip_usage,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn bool_true() -> bool { true }
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct AppConfigV1 {
|
||||||
|
pub version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_make: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_args: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_wsl_distro: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub project_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_obj: Option<ObjectConfigV1>,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub build_base: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_target: bool,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub rebuild_on_changes: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_update_check: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub watch_patterns: Vec<Glob>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recent_projects: Vec<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub diff_obj_config: DiffObjConfigV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfigV1 {
|
||||||
|
fn into_config(self) -> AppConfig {
|
||||||
|
log::info!("Upgrading configuration from v1");
|
||||||
|
AppConfig {
|
||||||
|
custom_make: self.custom_make,
|
||||||
|
custom_args: self.custom_args,
|
||||||
|
selected_wsl_distro: self.selected_wsl_distro,
|
||||||
|
project_dir: self.project_dir,
|
||||||
|
target_obj_dir: self.target_obj_dir,
|
||||||
|
base_obj_dir: self.base_obj_dir,
|
||||||
|
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
|
||||||
|
build_base: self.build_base,
|
||||||
|
build_target: self.build_target,
|
||||||
|
rebuild_on_changes: self.rebuild_on_changes,
|
||||||
|
auto_update_check: self.auto_update_check,
|
||||||
|
watch_patterns: self.watch_patterns,
|
||||||
|
recent_projects: self.recent_projects,
|
||||||
|
diff_obj_config: self.diff_obj_config.into_config(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ObjectConfigV0 {
|
pub struct ObjectConfigV0 {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -59,9 +238,7 @@ impl ObjectConfigV0 {
|
||||||
target_path: Some(self.target_path),
|
target_path: Some(self.target_path),
|
||||||
base_path: Some(self.base_path),
|
base_path: Some(self.base_path),
|
||||||
reverse_fn_order: self.reverse_fn_order,
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
complete: None,
|
..Default::default()
|
||||||
scratch: None,
|
|
||||||
source_path: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,11 @@ use anyhow::Result;
|
||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||||
|
|
||||||
use crate::app::AppState;
|
use crate::app::{AppState, ObjectConfig};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ProjectObjectNode {
|
pub enum ProjectObjectNode {
|
||||||
File(String, Box<ProjectObject>),
|
Unit(String, usize),
|
||||||
Dir(String, Vec<ProjectObjectNode>),
|
Dir(String, Vec<ProjectObjectNode>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,17 +33,18 @@ fn find_dir<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_nodes(
|
fn build_nodes(
|
||||||
objects: &[ProjectObject],
|
units: &mut [ProjectObject],
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
target_obj_dir: Option<&Path>,
|
target_obj_dir: Option<&Path>,
|
||||||
base_obj_dir: Option<&Path>,
|
base_obj_dir: Option<&Path>,
|
||||||
) -> Vec<ProjectObjectNode> {
|
) -> Vec<ProjectObjectNode> {
|
||||||
let mut nodes = vec![];
|
let mut nodes = vec![];
|
||||||
for object in objects {
|
for (idx, unit) in units.iter_mut().enumerate() {
|
||||||
|
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||||
let mut out_nodes = &mut nodes;
|
let mut out_nodes = &mut nodes;
|
||||||
let path = if let Some(name) = &object.name {
|
let path = if let Some(name) = &unit.name {
|
||||||
Path::new(name)
|
Path::new(name)
|
||||||
} else if let Some(path) = &object.path {
|
} else if let Some(path) = &unit.path {
|
||||||
path
|
path
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
|
@ -56,10 +57,8 @@ fn build_nodes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut object = Box::new(object.clone());
|
|
||||||
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
|
||||||
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||||
out_nodes.push(ProjectObjectNode::File(filename, object));
|
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
|
||||||
}
|
}
|
||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
|
@ -70,24 +69,36 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||||
};
|
};
|
||||||
if let Some((result, info)) = try_project_config(project_dir) {
|
if let Some((result, info)) = try_project_config(project_dir) {
|
||||||
let project_config = result?;
|
let project_config = result?;
|
||||||
state.config.custom_make = project_config.custom_make;
|
state.config.custom_make = project_config.custom_make.clone();
|
||||||
state.config.custom_args = project_config.custom_args;
|
state.config.custom_args = project_config.custom_args.clone();
|
||||||
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
|
state.config.target_obj_dir =
|
||||||
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
|
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
|
||||||
state.config.build_base = project_config.build_base;
|
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
|
||||||
state.config.build_target = project_config.build_target;
|
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||||
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||||
|
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||||
});
|
});
|
||||||
state.watcher_change = true;
|
state.watcher_change = true;
|
||||||
state.objects = project_config.objects;
|
state.objects = project_config.units.clone().unwrap_or_default();
|
||||||
state.object_nodes = build_nodes(
|
state.object_nodes = build_nodes(
|
||||||
&state.objects,
|
&mut state.objects,
|
||||||
project_dir,
|
project_dir,
|
||||||
state.config.target_obj_dir.as_deref(),
|
state.config.target_obj_dir.as_deref(),
|
||||||
state.config.base_obj_dir.as_deref(),
|
state.config.base_obj_dir.as_deref(),
|
||||||
);
|
);
|
||||||
|
state.current_project_config = Some(project_config);
|
||||||
state.project_config_info = Some(info);
|
state.project_config_info = Some(info);
|
||||||
|
|
||||||
|
// Reload selected object
|
||||||
|
if let Some(selected_obj) = &state.config.selected_obj {
|
||||||
|
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
|
||||||
|
let config = ObjectConfig::from(obj);
|
||||||
|
state.set_selected_obj(config);
|
||||||
|
} else {
|
||||||
|
state.clear_selected_obj();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl CreateScratchConfig {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
build_config: BuildConfig::from_config(config),
|
build_config: BuildConfig::from_config(config),
|
||||||
context_path: scratch_config.ctx_path.clone(),
|
context_path: scratch_config.ctx_path.clone(),
|
||||||
build_context: scratch_config.build_ctx,
|
build_context: scratch_config.build_ctx.unwrap_or(false),
|
||||||
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
||||||
platform: scratch_config.platform.clone().unwrap_or_default(),
|
platform: scratch_config.platform.clone().unwrap_or_default(),
|
||||||
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl JobQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether any job is running.
|
/// Returns whether any job is running.
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub fn any_running(&self) -> bool {
|
pub fn any_running(&self) -> bool {
|
||||||
self.jobs.iter().any(|job| {
|
self.jobs.iter().any(|job| {
|
||||||
if let Some(handle) = &job.handle {
|
if let Some(handle) = &job.handle {
|
||||||
|
|
|
@ -6,13 +6,13 @@ use std::{
|
||||||
|
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||||
obj::{read, ObjInfo},
|
obj::{read, ObjInfo},
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{AppConfig, ObjectConfig},
|
app::{AppConfig, AppState, ObjectConfig},
|
||||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -60,16 +60,20 @@ pub struct ObjDiffConfig {
|
||||||
pub build_target: bool,
|
pub build_target: bool,
|
||||||
pub selected_obj: Option<ObjectConfig>,
|
pub selected_obj: Option<ObjectConfig>,
|
||||||
pub diff_obj_config: DiffObjConfig,
|
pub diff_obj_config: DiffObjConfig,
|
||||||
|
pub selecting_left: Option<String>,
|
||||||
|
pub selecting_right: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjDiffConfig {
|
impl ObjDiffConfig {
|
||||||
pub(crate) fn from_config(config: &AppConfig) -> Self {
|
pub(crate) fn from_state(state: &AppState) -> Self {
|
||||||
Self {
|
Self {
|
||||||
build_config: BuildConfig::from_config(config),
|
build_config: BuildConfig::from_config(&state.config),
|
||||||
build_base: config.build_base,
|
build_base: state.config.build_base,
|
||||||
build_target: config.build_target,
|
build_target: state.config.build_target,
|
||||||
selected_obj: config.selected_obj.clone(),
|
selected_obj: state.config.selected_obj.clone(),
|
||||||
diff_obj_config: config.diff_obj_config.clone(),
|
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||||
|
selecting_left: state.selecting_left.clone(),
|
||||||
|
selecting_right: state.selecting_right.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,9 +162,16 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
||||||
fn run_build(
|
fn run_build(
|
||||||
context: &JobContext,
|
context: &JobContext,
|
||||||
cancel: Receiver<()>,
|
cancel: Receiver<()>,
|
||||||
config: ObjDiffConfig,
|
mut config: ObjDiffConfig,
|
||||||
) -> Result<Box<ObjDiffResult>> {
|
) -> Result<Box<ObjDiffResult>> {
|
||||||
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||||
|
// Use the per-object symbol mappings, we don't set mappings globally
|
||||||
|
config.diff_obj_config.symbol_mappings = MappingConfig {
|
||||||
|
mappings: obj_config.symbol_mappings,
|
||||||
|
selecting_left: config.selecting_left,
|
||||||
|
selecting_right: config.selecting_right,
|
||||||
|
};
|
||||||
|
|
||||||
let project_dir = config
|
let project_dir = config
|
||||||
.build_config
|
.build_config
|
||||||
.project_dir
|
.project_dir
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
use egui::{Align, Layout, Sense, Vec2};
|
||||||
|
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
|
||||||
|
|
||||||
|
pub fn render_header(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||||
|
) {
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
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);
|
||||||
|
for i in 0..num_columns {
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
add_contents(ui, i);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_table(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
row_height: f32,
|
||||||
|
total_rows: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut TableRow, usize),
|
||||||
|
) {
|
||||||
|
ui.style_mut().interaction.selectable_labels = false;
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
let table = TableBuilder::new(ui)
|
||||||
|
.striped(false)
|
||||||
|
.cell_layout(Layout::left_to_right(Align::Min))
|
||||||
|
.columns(Column::exact(column_width).clip(true), num_columns)
|
||||||
|
.resizable(false)
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.min_scrolled_height(available_height)
|
||||||
|
.sense(Sense::click());
|
||||||
|
table.body(|body| {
|
||||||
|
body.rows(row_height, total_rows, |mut row| {
|
||||||
|
row.set_hovered(false); // Disable hover effect
|
||||||
|
for i in 0..num_columns {
|
||||||
|
add_contents(&mut row, i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_strips(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||||
|
) {
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|
||||||
|
|mut strip| {
|
||||||
|
for i in 0..num_columns {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ui.push_id(i, |ui| {
|
||||||
|
add_contents(ui, i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ pub struct ConfigViewState {
|
||||||
pub build_running: bool,
|
pub build_running: bool,
|
||||||
pub queue_build: bool,
|
pub queue_build: bool,
|
||||||
pub watch_pattern_text: String,
|
pub watch_pattern_text: String,
|
||||||
pub load_error: Option<String>,
|
|
||||||
pub object_search: String,
|
pub object_search: String,
|
||||||
pub filter_diffable: bool,
|
pub filter_diffable: bool,
|
||||||
pub filter_incomplete: bool,
|
pub filter_incomplete: bool,
|
||||||
|
@ -93,10 +92,7 @@ impl ConfigViewState {
|
||||||
name: obj_path.display().to_string(),
|
name: obj_path.display().to_string(),
|
||||||
target_path: Some(target_path),
|
target_path: Some(target_path),
|
||||||
base_path: Some(path),
|
base_path: Some(path),
|
||||||
reverse_fn_order: None,
|
..Default::default()
|
||||||
complete: None,
|
|
||||||
scratch: None,
|
|
||||||
source_path: None,
|
|
||||||
});
|
});
|
||||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||||
let base_path = base_dir.join(obj_path);
|
let base_path = base_dir.join(obj_path);
|
||||||
|
@ -104,10 +100,7 @@ impl ConfigViewState {
|
||||||
name: obj_path.display().to_string(),
|
name: obj_path.display().to_string(),
|
||||||
target_path: Some(path),
|
target_path: Some(path),
|
||||||
base_path: Some(base_path),
|
base_path: Some(base_path),
|
||||||
reverse_fn_order: None,
|
..Default::default()
|
||||||
complete: None,
|
|
||||||
scratch: None,
|
|
||||||
source_path: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,7 +223,10 @@ pub fn config_ui(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut new_selected_obj = selected_obj.clone();
|
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
|
||||||
|
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
|
||||||
|
});
|
||||||
|
let mut new_selected_index = selected_index;
|
||||||
if objects.is_empty() {
|
if objects.is_empty() {
|
||||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||||
if ui.button("Select object").clicked() {
|
if ui.button("Select object").clicked() {
|
||||||
|
@ -316,6 +312,7 @@ pub fn config_ui(
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
for node in object_nodes.iter().filter_map(|node| {
|
for node in object_nodes.iter().filter_map(|node| {
|
||||||
filter_node(
|
filter_node(
|
||||||
|
objects,
|
||||||
node,
|
node,
|
||||||
&search,
|
&search,
|
||||||
config_state.filter_diffable,
|
config_state.filter_diffable,
|
||||||
|
@ -325,8 +322,9 @@ pub fn config_ui(
|
||||||
}) {
|
}) {
|
||||||
display_node(
|
display_node(
|
||||||
ui,
|
ui,
|
||||||
&mut new_selected_obj,
|
&mut new_selected_index,
|
||||||
project_dir.as_deref(),
|
project_dir.as_deref(),
|
||||||
|
objects,
|
||||||
&node,
|
&node,
|
||||||
appearance,
|
appearance,
|
||||||
node_open,
|
node_open,
|
||||||
|
@ -334,10 +332,11 @@ pub fn config_ui(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if new_selected_obj != *selected_obj {
|
if new_selected_index != selected_index {
|
||||||
if let Some(obj) = new_selected_obj {
|
if let Some(idx) = new_selected_index {
|
||||||
// Will set obj_changed, which will trigger a rebuild
|
// Will set obj_changed, which will trigger a rebuild
|
||||||
state_guard.set_selected_obj(obj);
|
let config = ObjectConfig::from(&objects[idx]);
|
||||||
|
state_guard.set_selected_obj(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if state_guard.config.selected_obj.is_some()
|
if state_guard.config.selected_obj.is_some()
|
||||||
|
@ -347,16 +346,17 @@ pub fn config_ui(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_object(
|
fn display_unit(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<usize>,
|
||||||
project_dir: Option<&Path>,
|
project_dir: Option<&Path>,
|
||||||
name: &str,
|
name: &str,
|
||||||
object: &ProjectObject,
|
units: &[ProjectObject],
|
||||||
|
index: usize,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let object_name = object.name();
|
let object = &units[index];
|
||||||
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
let selected = *selected_obj == Some(index);
|
||||||
let color = if selected {
|
let color = if selected {
|
||||||
appearance.emphasized_text_color
|
appearance.emphasized_text_color
|
||||||
} else if let Some(complete) = object.complete() {
|
} else if let Some(complete) = object.complete() {
|
||||||
|
@ -381,18 +381,8 @@ fn display_object(
|
||||||
if get_source_path(project_dir, object).is_some() {
|
if get_source_path(project_dir, object).is_some() {
|
||||||
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||||
}
|
}
|
||||||
// Always recreate ObjectConfig if selected, in case the project config changed.
|
if response.clicked() {
|
||||||
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
*selected_obj = Some(index);
|
||||||
if selected || response.clicked() {
|
|
||||||
*selected_obj = Some(ObjectConfig {
|
|
||||||
name: object_name.to_string(),
|
|
||||||
target_path: object.target_path.clone(),
|
|
||||||
base_path: object.base_path.clone(),
|
|
||||||
reverse_fn_order: object.reverse_fn_order(),
|
|
||||||
complete: object.complete(),
|
|
||||||
scratch: object.scratch.clone(),
|
|
||||||
source_path: object.source_path().cloned(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,18 +417,19 @@ enum NodeOpen {
|
||||||
|
|
||||||
fn display_node(
|
fn display_node(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<usize>,
|
||||||
project_dir: Option<&Path>,
|
project_dir: Option<&Path>,
|
||||||
|
units: &[ProjectObject],
|
||||||
node: &ProjectObjectNode,
|
node: &ProjectObjectNode,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
node_open: NodeOpen,
|
node_open: NodeOpen,
|
||||||
) {
|
) {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(name, object) => {
|
ProjectObjectNode::Unit(name, idx) => {
|
||||||
display_object(ui, selected_obj, project_dir, name, object, appearance);
|
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
|
||||||
}
|
}
|
||||||
ProjectObjectNode::Dir(name, children) => {
|
ProjectObjectNode::Dir(name, children) => {
|
||||||
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
|
||||||
let open = match node_open {
|
let open = match node_open {
|
||||||
NodeOpen::Default => None,
|
NodeOpen::Default => None,
|
||||||
NodeOpen::Open => Some(true),
|
NodeOpen::Open => Some(true),
|
||||||
|
@ -461,16 +452,16 @@ fn display_node(
|
||||||
.open(open)
|
.open(open)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for node in children {
|
for node in children {
|
||||||
display_node(ui, selected_obj, project_dir, node, appearance, node_open);
|
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
|
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
|
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
|
||||||
ProjectObjectNode::Dir(_, children) => {
|
ProjectObjectNode::Dir(_, children) => {
|
||||||
children.iter().any(|node| contains_node(node, selected_obj))
|
children.iter().any(|node| contains_node(node, selected_obj))
|
||||||
}
|
}
|
||||||
|
@ -478,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_node(
|
fn filter_node(
|
||||||
|
units: &[ProjectObject],
|
||||||
node: &ProjectObjectNode,
|
node: &ProjectObjectNode,
|
||||||
search: &str,
|
search: &str,
|
||||||
filter_diffable: bool,
|
filter_diffable: bool,
|
||||||
|
@ -485,12 +477,12 @@ fn filter_node(
|
||||||
show_hidden: bool,
|
show_hidden: bool,
|
||||||
) -> Option<ProjectObjectNode> {
|
) -> Option<ProjectObjectNode> {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(name, object) => {
|
ProjectObjectNode::Unit(name, idx) => {
|
||||||
|
let unit = &units[*idx];
|
||||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||||
&& (!filter_diffable
|
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|
||||||
|| (object.base_path.is_some() && object.target_path.is_some()))
|
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
|
||||||
&& (!filter_incomplete || matches!(object.complete(), None | Some(false)))
|
&& (show_hidden || !unit.hidden())
|
||||||
&& (show_hidden || !object.hidden())
|
|
||||||
{
|
{
|
||||||
Some(node.clone())
|
Some(node.clone())
|
||||||
} else {
|
} else {
|
||||||
|
@ -501,7 +493,14 @@ fn filter_node(
|
||||||
let new_children = children
|
let new_children = children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|child| {
|
.filter_map(|child| {
|
||||||
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden)
|
filter_node(
|
||||||
|
units,
|
||||||
|
child,
|
||||||
|
search,
|
||||||
|
filter_diffable,
|
||||||
|
filter_incomplete,
|
||||||
|
show_hidden,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !new_children.is_empty() {
|
if !new_children.is_empty() {
|
||||||
|
@ -570,14 +569,14 @@ pub fn project_window(
|
||||||
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(error) = &config_state.load_error {
|
if let Some(error) = &state_guard.config_error {
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||||
ui.label("Failed to load project config:");
|
ui.label("Failed to load project config:");
|
||||||
ui.colored_label(appearance.delete_color, error);
|
ui.colored_label(appearance.delete_color, error);
|
||||||
});
|
});
|
||||||
if !open {
|
if !open {
|
||||||
config_state.load_error = None;
|
state_guard.config_error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{cmp::min, default::Default, mem::take};
|
use std::{cmp::min, default::Default, mem::take};
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
||||||
obj::ObjInfo,
|
obj::ObjInfo,
|
||||||
|
@ -10,14 +9,15 @@ use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
appearance::Appearance,
|
appearance::Appearance,
|
||||||
symbol_diff::{DiffViewState, SymbolRefByName, View},
|
column_layout::{render_header, render_table},
|
||||||
|
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||||
write_text,
|
write_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BYTES_PER_ROW: usize = 16;
|
const BYTES_PER_ROW: usize = 16;
|
||||||
|
|
||||||
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> {
|
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
|
||||||
obj.sections.iter().position(|section| section.name == selected_symbol.section_name)
|
obj.sections.iter().position(|section| section.name == section_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
||||||
|
@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
||||||
split_diffs
|
split_diffs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SectionDiffContext<'a> {
|
||||||
|
obj: &'a ObjInfo,
|
||||||
|
diff: &'a ObjDiff,
|
||||||
|
section_index: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SectionDiffContext<'a> {
|
||||||
|
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
|
||||||
|
obj.map(|(obj, diff)| Self {
|
||||||
|
obj,
|
||||||
|
diff,
|
||||||
|
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_section(&self) -> bool { self.section_index.is_some() }
|
||||||
|
}
|
||||||
|
|
||||||
fn data_table_ui(
|
fn data_table_ui(
|
||||||
table: TableBuilder<'_>,
|
ui: &mut egui::Ui,
|
||||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
available_width: f32,
|
||||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
left_ctx: Option<SectionDiffContext<'_>>,
|
||||||
selected_symbol: &SymbolRefByName,
|
right_ctx: Option<SectionDiffContext<'_>>,
|
||||||
config: &Appearance,
|
config: &Appearance,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let left_section = left_obj.and_then(|(obj, diff)| {
|
let left_section = left_ctx
|
||||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||||
});
|
let right_section = right_ctx
|
||||||
let right_section = right_obj.and_then(|(obj, diff)| {
|
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
|
||||||
});
|
|
||||||
|
|
||||||
let total_bytes = left_section
|
let total_bytes = left_section
|
||||||
.or(right_section)?
|
.or(right_section)?
|
||||||
.1
|
.1
|
||||||
|
@ -159,118 +176,117 @@ fn data_table_ui(
|
||||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||||
|
|
||||||
table.body(|body| {
|
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||||
body.rows(config.code_font.size, total_rows, |mut row| {
|
let i = row.index();
|
||||||
let row_index = row.index();
|
let address = i * BYTES_PER_ROW;
|
||||||
let address = row_index * BYTES_PER_ROW;
|
row.col(|ui| {
|
||||||
row.col(|ui| {
|
if column == 0 {
|
||||||
if let Some(left_diffs) = &left_diffs {
|
if let Some(left_diffs) = &left_diffs {
|
||||||
data_row_ui(ui, address, &left_diffs[row_index], config);
|
data_row_ui(ui, address, &left_diffs[i], config);
|
||||||
}
|
}
|
||||||
});
|
} else if column == 1 {
|
||||||
row.col(|ui| {
|
|
||||||
if let Some(right_diffs) = &right_diffs {
|
if let Some(right_diffs) = &right_diffs {
|
||||||
data_row_ui(ui, address, &right_diffs[row_index], config);
|
data_row_ui(ui, address, &right_diffs[i], config);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[must_use]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub fn data_diff_ui(
|
||||||
else {
|
ui: &mut egui::Ui,
|
||||||
return;
|
state: &DiffViewState,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
|
let Some(result) = &state.build else {
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let section_name =
|
||||||
|
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|
||||||
|
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
|
||||||
|
);
|
||||||
|
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
|
||||||
|
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||||
|
|
||||||
|
// If both sides are missing a symbol, switch to symbol diff view
|
||||||
|
if !right_ctx.map_or(false, |ctx| ctx.has_section())
|
||||||
|
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
|
||||||
|
{
|
||||||
|
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|
}
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
ui.allocate_ui_with_layout(
|
if column == 0 {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
|
||||||
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
if ui.button("⏴ Back").clicked() {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
Layout::top_down(Align::Min),
|
}
|
||||||
|ui| {
|
|
||||||
ui.set_width(column_width);
|
|
||||||
|
|
||||||
if ui.button("⏴ Back").clicked() {
|
|
||||||
state.current_view = View::SymbolDiff;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if let Some(section) =
|
||||||
|
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||||
|
{
|
||||||
|
ui.label(
|
||||||
|
RichText::new(section.name.clone())
|
||||||
|
.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
|
// Right column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Build);
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
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.horizontal(|ui| {
|
if let Some(section) =
|
||||||
if ui
|
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
{
|
||||||
.clicked()
|
ui.label(
|
||||||
{
|
RichText::new(section.name.clone())
|
||||||
state.queue_build = true;
|
.font(appearance.code_font.clone())
|
||||||
}
|
.color(appearance.highlight_color),
|
||||||
ui.scope(|ui| {
|
);
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
} else {
|
||||||
if state.build_running {
|
ui.label(
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
RichText::new("Missing")
|
||||||
} else {
|
.font(appearance.code_font.clone())
|
||||||
ui.label("Last built:");
|
.color(appearance.replace_color),
|
||||||
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);
|
|
||||||
ui.label("");
|
|
||||||
ui.label("Diff base:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
ui.style_mut().interaction.selectable_labels = false;
|
let id =
|
||||||
let available_height = ui.available_height();
|
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
|
||||||
let table = TableBuilder::new(ui)
|
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
|
||||||
.striped(false)
|
ui.push_id(id, |ui| {
|
||||||
.cell_layout(Layout::left_to_right(Align::Min))
|
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
|
||||||
.columns(Column::exact(column_width).clip(true), 2)
|
});
|
||||||
.resizable(false)
|
ret
|
||||||
.auto_shrink([false, false])
|
|
||||||
.min_scrolled_height(available_height);
|
|
||||||
data_table_ui(
|
|
||||||
table,
|
|
||||||
result.first_obj.as_ref(),
|
|
||||||
result.second_obj.as_ref(),
|
|
||||||
selected_symbol,
|
|
||||||
appearance,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,20 @@
|
||||||
use egui::{Align, Layout, ScrollArea, Ui, Vec2};
|
use egui::{RichText, ScrollArea};
|
||||||
use egui_extras::{Size, StripBuilder};
|
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
arch::ppc::ExceptionInfo,
|
arch::ppc::ExceptionInfo,
|
||||||
diff::ObjDiff,
|
obj::{ObjInfo, ObjSymbol},
|
||||||
obj::{ObjInfo, ObjSymbol, SymbolRef},
|
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
appearance::Appearance,
|
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 {
|
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||||
let mut text = String::from("");
|
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(
|
fn extab_text_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut egui::Ui,
|
||||||
obj: &(ObjInfo, ObjDiff),
|
ctx: FunctionDiffContext<'_>,
|
||||||
symbol_ref: SymbolRef,
|
symbol: &ObjSymbol,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||||
|
|
||||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
|
||||||
let text = decode_extab(extab_entry);
|
let text = decode_extab(extab_entry);
|
||||||
ui.colored_label(appearance.replace_color, &text);
|
ui.colored_label(appearance.replace_color, &text);
|
||||||
return Some(());
|
return Some(());
|
||||||
|
@ -65,137 +55,194 @@ fn extab_text_ui(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extab_ui(
|
fn extab_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut egui::Ui,
|
||||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
ctx: FunctionDiffContext<'_>,
|
||||||
selected_symbol: &SymbolRefByName,
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
_left: bool,
|
_column: usize,
|
||||||
) {
|
) {
|
||||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
if let Some((_section, symbol)) =
|
||||||
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
{
|
||||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
extab_text_ui(ui, ctx, symbol, appearance);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[must_use]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub fn extab_diff_ui(
|
||||||
else {
|
ui: &mut egui::Ui,
|
||||||
return;
|
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
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
ui.allocate_ui_with_layout(
|
if column == 0 {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
|
||||||
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.button("⏴ Back").clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.separator();
|
||||||
|
if ui
|
||||||
ui.horizontal(|ui| {
|
.add_enabled(
|
||||||
if ui.button("⏴ Back").clicked() {
|
!state.scratch_running
|
||||||
state.current_view = View::SymbolDiff;
|
&& state.scratch_available
|
||||||
}
|
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||||
});
|
egui::Button::new("📲 decomp.me"),
|
||||||
|
)
|
||||||
let name = selected_symbol
|
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||||
.demangled_symbol_name
|
.on_disabled_hover_text("Scratch configuration missing")
|
||||||
.as_deref()
|
.clicked()
|
||||||
.unwrap_or(&selected_symbol.symbol_name);
|
{
|
||||||
ui.scope(|ui| {
|
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
ui.colored_label(appearance.highlight_color, name);
|
}) {
|
||||||
ui.label("Diff target:");
|
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
|
// Right column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Build);
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
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 let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||||
if ui
|
ctx.symbol_ref.map(|symbol_ref| {
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||||
.clicked()
|
})
|
||||||
{
|
}) {
|
||||||
state.queue_build = true;
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
}
|
ui.label(
|
||||||
ui.scope(|ui| {
|
RichText::new(name)
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
.font(appearance.code_font.clone())
|
||||||
if state.build_running {
|
.color(appearance.highlight_color),
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
);
|
||||||
} else {
|
if let Some(match_percent) = symbol_diff.match_percent {
|
||||||
ui.label("Last built:");
|
ui.label(
|
||||||
let format =
|
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
.font(appearance.code_font.clone())
|
||||||
ui.label(
|
.color(match_color_for_symbol(match_percent, appearance)),
|
||||||
result
|
);
|
||||||
.time
|
}
|
||||||
.to_offset(appearance.utc_offset)
|
} else {
|
||||||
.format(&format)
|
ui.label(
|
||||||
.unwrap(),
|
RichText::new("Missing")
|
||||||
);
|
.font(appearance.code_font.clone())
|
||||||
}
|
.color(appearance.replace_color),
|
||||||
});
|
);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
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();
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
render_strips(ui, available_width, 2, |ui, column| {
|
||||||
strip.strip(|builder| {
|
if column == 0 {
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
if let Some(ctx) = left_ctx {
|
||||||
strip.cell(|ui| {
|
extab_ui(ui, ctx, appearance, column);
|
||||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
}
|
||||||
});
|
} else if column == 1 {
|
||||||
strip.cell(|ui| {
|
if let Some(ctx) = right_ctx {
|
||||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
extab_ui(ui, ctx, appearance, column);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget};
|
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
|
||||||
use egui_extras::{Column, TableBuilder, TableRow};
|
use egui_extras::TableRow;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
arch::ObjArch,
|
arch::ObjArch,
|
||||||
diff::{
|
diff::{
|
||||||
display::{display_diff, DiffText, HighlightKind},
|
display::{display_diff, DiffText, HighlightKind},
|
||||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||||
},
|
},
|
||||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
|
||||||
|
SymbolRef,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
appearance::Appearance,
|
appearance::Appearance,
|
||||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
column_layout::{render_header, render_strips, render_table},
|
||||||
|
symbol_diff::{
|
||||||
|
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
|
||||||
|
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
||||||
enum ColumnId {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FunctionViewState {
|
pub struct FunctionViewState {
|
||||||
left_highlight: HighlightKind,
|
left_highlight: HighlightKind,
|
||||||
|
@ -30,16 +31,17 @@ pub struct FunctionViewState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FunctionViewState {
|
impl FunctionViewState {
|
||||||
fn highlight(&self, column: ColumnId) -> &HighlightKind {
|
pub fn highlight(&self, column: usize) -> &HighlightKind {
|
||||||
match column {
|
match column {
|
||||||
ColumnId::Left => &self.left_highlight,
|
0 => &self.left_highlight,
|
||||||
ColumnId::Right => &self.right_highlight,
|
1 => &self.right_highlight,
|
||||||
|
_ => &HighlightKind::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) {
|
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
|
||||||
match column {
|
match column {
|
||||||
ColumnId::Left => {
|
0 => {
|
||||||
if highlight == self.left_highlight {
|
if highlight == self.left_highlight {
|
||||||
if highlight == self.right_highlight {
|
if highlight == self.right_highlight {
|
||||||
self.left_highlight = HighlightKind::None;
|
self.left_highlight = HighlightKind::None;
|
||||||
|
@ -51,7 +53,7 @@ impl FunctionViewState {
|
||||||
self.left_highlight = highlight;
|
self.left_highlight = highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ColumnId::Right => {
|
1 => {
|
||||||
if highlight == self.right_highlight {
|
if highlight == self.right_highlight {
|
||||||
if highlight == self.left_highlight {
|
if highlight == self.left_highlight {
|
||||||
self.left_highlight = HighlightKind::None;
|
self.left_highlight = HighlightKind::None;
|
||||||
|
@ -63,10 +65,11 @@ impl FunctionViewState {
|
||||||
self.right_highlight = highlight;
|
self.right_highlight = highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_highlight(&mut self) {
|
pub fn clear_highlight(&mut self) {
|
||||||
self.left_highlight = HighlightKind::None;
|
self.left_highlight = HighlightKind::None;
|
||||||
self.right_highlight = HighlightKind::None;
|
self.right_highlight = HighlightKind::None;
|
||||||
}
|
}
|
||||||
|
@ -223,17 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[must_use]
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn diff_text_ui(
|
fn diff_text_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
text: DiffText<'_>,
|
text: DiffText<'_>,
|
||||||
ins_diff: &ObjInsDiff,
|
ins_diff: &ObjInsDiff,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
column: ColumnId,
|
column: usize,
|
||||||
space_width: f32,
|
space_width: f32,
|
||||||
response_cb: impl Fn(Response) -> Response,
|
response_cb: impl Fn(Response) -> Response,
|
||||||
) {
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
let label_text;
|
let label_text;
|
||||||
let mut base_color = match ins_diff.kind {
|
let mut base_color = match ins_diff.kind {
|
||||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||||
|
@ -287,7 +292,7 @@ fn diff_text_ui(
|
||||||
}
|
}
|
||||||
DiffText::Spacing(n) => {
|
DiffText::Spacing(n) => {
|
||||||
ui.add_space(n as f32 * space_width);
|
ui.add_space(n as f32 * space_width);
|
||||||
return;
|
return ret;
|
||||||
}
|
}
|
||||||
DiffText::Eol => {
|
DiffText::Eol => {
|
||||||
label_text = "\n".to_string();
|
label_text = "\n".to_string();
|
||||||
|
@ -304,22 +309,25 @@ fn diff_text_ui(
|
||||||
.ui(ui);
|
.ui(ui);
|
||||||
response = response_cb(response);
|
response = response_cb(response);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
ins_view_state.set_highlight(column, text.into());
|
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
|
||||||
}
|
}
|
||||||
if len < pad_to {
|
if len < pad_to {
|
||||||
ui.add_space((pad_to - len) as f32 * space_width);
|
ui.add_space((pad_to - len) as f32 * space_width);
|
||||||
}
|
}
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn asm_row_ui(
|
fn asm_row_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
ins_diff: &ObjInsDiff,
|
ins_diff: &ObjInsDiff,
|
||||||
symbol: &ObjSymbol,
|
symbol: &ObjSymbol,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
column: ColumnId,
|
column: usize,
|
||||||
response_cb: impl Fn(Response) -> Response,
|
response_cb: impl Fn(Response) -> Response,
|
||||||
) {
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
if ins_diff.kind != ObjInsDiffKind::None {
|
if ins_diff.kind != ObjInsDiffKind::None {
|
||||||
|
@ -327,7 +335,7 @@ fn asm_row_ui(
|
||||||
}
|
}
|
||||||
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||||
display_diff(ins_diff, symbol.address, |text| {
|
display_diff(ins_diff, symbol.address, |text| {
|
||||||
diff_text_ui(
|
if let Some(action) = diff_text_ui(
|
||||||
ui,
|
ui,
|
||||||
text,
|
text,
|
||||||
ins_diff,
|
ins_diff,
|
||||||
|
@ -336,246 +344,476 @@ fn asm_row_ui(
|
||||||
column,
|
column,
|
||||||
space_width,
|
space_width,
|
||||||
&response_cb,
|
&response_cb,
|
||||||
);
|
) {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
Ok::<_, ()>(())
|
Ok::<_, ()>(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn asm_col_ui(
|
fn asm_col_ui(
|
||||||
row: &mut TableRow<'_, '_>,
|
row: &mut TableRow<'_, '_>,
|
||||||
obj: &(ObjInfo, ObjDiff),
|
ctx: FunctionDiffContext<'_>,
|
||||||
symbol_ref: SymbolRef,
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
column: ColumnId,
|
column: usize,
|
||||||
) {
|
) -> Option<DiffViewAction> {
|
||||||
let (section, symbol) = obj.0.section_symbol(symbol_ref);
|
let mut ret = None;
|
||||||
let section = section.unwrap();
|
let symbol_ref = ctx.symbol_ref?;
|
||||||
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
|
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||||
|
let section = section?;
|
||||||
|
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
|
||||||
let response_cb = |response: Response| {
|
let response_cb = |response: Response| {
|
||||||
if let Some(ins) = &ins_diff.ins {
|
if let Some(ins) = &ins_diff.ins {
|
||||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||||
response.on_hover_ui_at_pointer(|ui| {
|
response.on_hover_ui_at_pointer(|ui| {
|
||||||
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance)
|
ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (_, response) = row.col(|ui| {
|
let (_, response) = row.col(|ui| {
|
||||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb);
|
if let Some(action) =
|
||||||
|
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
response_cb(response);
|
response_cb(response);
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
|
#[must_use]
|
||||||
row.col(|ui| {
|
|
||||||
ui.label("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn asm_table_ui(
|
fn asm_table_ui(
|
||||||
table: TableBuilder<'_>,
|
ui: &mut egui::Ui,
|
||||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
available_width: f32,
|
||||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
left_ctx: Option<FunctionDiffContext<'_>>,
|
||||||
selected_symbol: &SymbolRefByName,
|
right_ctx: Option<FunctionDiffContext<'_>>,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
) -> Option<()> {
|
symbol_state: &SymbolViewState,
|
||||||
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
) -> Option<DiffViewAction> {
|
||||||
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
let mut ret = None;
|
||||||
let instructions_len = match (left_symbol, right_symbol) {
|
let left_len = left_ctx.and_then(|ctx| {
|
||||||
(Some(left_symbol_ref), Some(right_symbol_ref)) => {
|
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||||
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len();
|
});
|
||||||
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len();
|
let right_len = right_ctx.and_then(|ctx| {
|
||||||
debug_assert_eq!(left_len, right_len);
|
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||||
|
});
|
||||||
|
let instructions_len = match (left_len, right_len) {
|
||||||
|
(Some(left_len), Some(right_len)) => {
|
||||||
|
if left_len != right_len {
|
||||||
|
ui.label("Instruction count mismatch");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
left_len
|
left_len
|
||||||
}
|
}
|
||||||
(Some(left_symbol_ref), None) => {
|
(Some(left_len), None) => left_len,
|
||||||
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len()
|
(None, Some(right_len)) => right_len,
|
||||||
|
(None, None) => {
|
||||||
|
ui.label("No symbol selected");
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
(None, Some(right_symbol_ref)) => {
|
|
||||||
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
|
|
||||||
}
|
|
||||||
(None, None) => return None,
|
|
||||||
};
|
};
|
||||||
table.body(|body| {
|
if left_len.is_some() && right_len.is_some() {
|
||||||
body.rows(appearance.code_font.size, instructions_len, |mut row| {
|
// Joint view
|
||||||
row.set_hovered(false); // Disable row hover effect
|
render_table(
|
||||||
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
|
ui,
|
||||||
asm_col_ui(
|
available_width,
|
||||||
&mut row,
|
2,
|
||||||
left_obj,
|
appearance.code_font.size,
|
||||||
left_symbol_ref,
|
instructions_len,
|
||||||
appearance,
|
|row, column| {
|
||||||
ins_view_state,
|
if column == 0 {
|
||||||
ColumnId::Left,
|
if let Some(ctx) = left_ctx {
|
||||||
);
|
if let Some(action) =
|
||||||
} else {
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
empty_col_ui(&mut row);
|
{
|
||||||
}
|
ret = Some(action);
|
||||||
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
|
}
|
||||||
asm_col_ui(
|
}
|
||||||
&mut row,
|
} else if column == 1 {
|
||||||
right_obj,
|
if let Some(ctx) = right_ctx {
|
||||||
right_symbol_ref,
|
if let Some(action) =
|
||||||
appearance,
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
ins_view_state,
|
{
|
||||||
ColumnId::Right,
|
ret = Some(action);
|
||||||
);
|
}
|
||||||
} else {
|
}
|
||||||
empty_col_ui(&mut row);
|
if row.response().clicked() {
|
||||||
}
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
if row.response().clicked() {
|
}
|
||||||
ins_view_state.clear_highlight();
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Split view, one side is the symbol list
|
||||||
|
render_strips(ui, available_width, 2, |ui, column| {
|
||||||
|
if column == 0 {
|
||||||
|
if let Some(ctx) = left_ctx {
|
||||||
|
if ctx.has_symbol() {
|
||||||
|
render_table(
|
||||||
|
ui,
|
||||||
|
available_width / 2.0,
|
||||||
|
1,
|
||||||
|
appearance.code_font.size,
|
||||||
|
instructions_len,
|
||||||
|
|row, column| {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
if row.response().clicked() {
|
||||||
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if let Some((right_ctx, right_symbol_ref)) =
|
||||||
|
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||||
|
{
|
||||||
|
if let Some(action) = symbol_list_ui(
|
||||||
|
ui,
|
||||||
|
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||||
|
None,
|
||||||
|
symbol_state,
|
||||||
|
SymbolFilter::Mapping(right_symbol_ref),
|
||||||
|
appearance,
|
||||||
|
column,
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
left_symbol: Some(left_symbol_ref),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (right_section, right_symbol) =
|
||||||
|
right_ctx.obj.section_symbol(right_symbol_ref);
|
||||||
|
ret = Some(DiffViewAction::SetMapping(
|
||||||
|
match right_section.map(|s| s.kind) {
|
||||||
|
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||||
|
_ => View::SymbolDiff,
|
||||||
|
},
|
||||||
|
left_symbol_ref,
|
||||||
|
SymbolRefByName::new(right_symbol, right_section),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("No left object");
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
|
if let Some(ctx) = right_ctx {
|
||||||
|
if ctx.has_symbol() {
|
||||||
|
render_table(
|
||||||
|
ui,
|
||||||
|
available_width / 2.0,
|
||||||
|
1,
|
||||||
|
appearance.code_font.size,
|
||||||
|
instructions_len,
|
||||||
|
|row, column| {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
if row.response().clicked() {
|
||||||
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if let Some((left_ctx, left_symbol_ref)) =
|
||||||
|
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||||
|
{
|
||||||
|
if let Some(action) = symbol_list_ui(
|
||||||
|
ui,
|
||||||
|
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||||
|
None,
|
||||||
|
symbol_state,
|
||||||
|
SymbolFilter::Mapping(left_symbol_ref),
|
||||||
|
appearance,
|
||||||
|
column,
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
right_symbol: Some(right_symbol_ref),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (left_section, left_symbol) =
|
||||||
|
left_ctx.obj.section_symbol(left_symbol_ref);
|
||||||
|
ret = Some(DiffViewAction::SetMapping(
|
||||||
|
match left_section.map(|s| s.kind) {
|
||||||
|
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||||
|
_ => View::SymbolDiff,
|
||||||
|
},
|
||||||
|
SymbolRefByName::new(left_symbol, left_section),
|
||||||
|
right_symbol_ref,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("No right object");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
Some(())
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[derive(Clone, Copy)]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub struct FunctionDiffContext<'a> {
|
||||||
else {
|
pub obj: &'a ObjInfo,
|
||||||
return;
|
pub diff: &'a ObjDiff,
|
||||||
|
pub symbol_ref: Option<SymbolRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FunctionDiffContext<'a> {
|
||||||
|
pub fn new(
|
||||||
|
obj: Option<&'a (ObjInfo, ObjDiff)>,
|
||||||
|
selected_symbol: Option<&SymbolRefByName>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
obj.map(|(obj, diff)| Self {
|
||||||
|
obj,
|
||||||
|
diff,
|
||||||
|
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn function_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
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
ui.allocate_ui_with_layout(
|
if column == 0 {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
|
||||||
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.button("⏴ Back").clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.separator();
|
||||||
|
if ui
|
||||||
ui.horizontal(|ui| {
|
.add_enabled(
|
||||||
if ui.button("⏴ Back").clicked() {
|
!state.scratch_running
|
||||||
state.current_view = View::SymbolDiff;
|
&& state.scratch_available
|
||||||
}
|
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||||
ui.separator();
|
egui::Button::new("📲 decomp.me"),
|
||||||
if ui
|
)
|
||||||
.add_enabled(
|
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||||
!state.scratch_running && state.scratch_available,
|
.on_disabled_hover_text("Scratch configuration missing")
|
||||||
egui::Button::new("📲 decomp.me"),
|
.clicked()
|
||||||
)
|
{
|
||||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||||
.on_disabled_hover_text("Scratch configuration missing")
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
.clicked()
|
}) {
|
||||||
{
|
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||||
state.queue_scratch = true;
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
if right_ctx.map_or(false, |m| m.has_symbol())
|
||||||
|
&& ui
|
||||||
|
.button("Change target")
|
||||||
|
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
|
||||||
|
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Missing")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.replace_color),
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Choose target symbol")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
// Right column
|
// Right column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Build);
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
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 let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||||
if ui
|
ctx.symbol_ref.map(|symbol_ref| {
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||||
.clicked()
|
})
|
||||||
{
|
}) {
|
||||||
state.queue_build = true;
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
}
|
ui.label(
|
||||||
ui.scope(|ui| {
|
RichText::new(name)
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
.font(appearance.code_font.clone())
|
||||||
if state.build_running {
|
.color(appearance.highlight_color),
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
);
|
||||||
} else {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Last built:");
|
if let Some(match_percent) = symbol_diff.match_percent {
|
||||||
let format =
|
ui.label(
|
||||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||||
ui.label(
|
.font(appearance.code_font.clone())
|
||||||
result
|
.color(match_color_for_symbol(match_percent, appearance)),
|
||||||
.time
|
);
|
||||||
.to_offset(appearance.utc_offset)
|
}
|
||||||
.format(&format)
|
if left_ctx.map_or(false, |m| m.has_symbol()) {
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(
|
.button("Change base")
|
||||||
state.source_path_available,
|
.on_hover_text_at_pointer(
|
||||||
egui::Button::new("🖹 Source file"),
|
"Choose a different symbol to use as the base",
|
||||||
)
|
)
|
||||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
|
||||||
.on_disabled_hover_text("Source file metadata missing")
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.queue_open_source_path = true;
|
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
|
||||||
|
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
ui.scope(|ui| {
|
} else {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.label(
|
||||||
if let Some(match_percent) = result
|
RichText::new("Missing")
|
||||||
.second_obj
|
.font(appearance.code_font.clone())
|
||||||
.as_ref()
|
.color(appearance.replace_color),
|
||||||
.and_then(|(obj, diff)| {
|
);
|
||||||
find_symbol(obj, selected_symbol).map(|sref| {
|
ui.label(
|
||||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
RichText::new("Choose base symbol")
|
||||||
})
|
.font(appearance.code_font.clone())
|
||||||
})
|
.color(appearance.highlight_color),
|
||||||
.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();
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
ui.style_mut().interaction.selectable_labels = false;
|
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
|
||||||
let available_height = ui.available_height();
|
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
|
||||||
let table = TableBuilder::new(ui)
|
if let Some(action) = ui
|
||||||
.striped(false)
|
.push_id(id, |ui| {
|
||||||
.cell_layout(Layout::left_to_right(Align::Min))
|
asm_table_ui(
|
||||||
.columns(Column::exact(column_width).clip(true), 2)
|
ui,
|
||||||
.resizable(false)
|
available_width,
|
||||||
.auto_shrink([false, false])
|
left_ctx,
|
||||||
.min_scrolled_height(available_height)
|
right_ctx,
|
||||||
.sense(Sense::click());
|
appearance,
|
||||||
asm_table_ui(
|
&state.function_state,
|
||||||
table,
|
&state.symbol_state,
|
||||||
result.first_obj.as_ref(),
|
)
|
||||||
result.second_obj.as_ref(),
|
})
|
||||||
selected_symbol,
|
.inner
|
||||||
appearance,
|
{
|
||||||
&mut state.function_state,
|
ret = Some(action);
|
||||||
);
|
}
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||||
|
|
||||||
pub(crate) mod appearance;
|
pub(crate) mod appearance;
|
||||||
|
pub(crate) mod column_layout;
|
||||||
pub(crate) mod config;
|
pub(crate) mod config;
|
||||||
pub(crate) mod data_diff;
|
pub(crate) mod data_diff;
|
||||||
pub(crate) mod debug;
|
pub(crate) mod debug;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue