From 52c138bf06261c515160bd2a4c0a93d154422944 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 15 Aug 2025 16:24:26 -0600 Subject: [PATCH] Add "ignore_patterns" option to config This allows explicitly ignoring changes to certain files or directories, even if the changed file ends up matching `watch_patterns`. The default value, `build/**/*` will ensure that changes in the build directory will not trigger a duplicate rebuild. Resolves #143 Resolves #215 --- README.md | 7 +++ config.schema.json | 10 +++++ objdiff-cli/src/cmd/diff.rs | 2 + objdiff-core/src/build/watcher.rs | 5 ++- objdiff-core/src/config/mod.rs | 21 ++++++++- objdiff-gui/src/app.rs | 19 +++++--- objdiff-gui/src/config.rs | 13 ++++-- objdiff-gui/src/views/config.rs | 65 ++++++++++++++++++++-------- objdiff-gui/src/views/symbol_diff.rs | 5 +++ 9 files changed, 119 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5700d76..c53d4b8 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,9 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it "*.txt", "*.json" ], + "ignore_patterns": [ + "build/**/*" + ], "units": [ { "name": "main/MetroTRK/mslsupp", @@ -141,6 +144,10 @@ It's unlikely you'll want to disable this, unless you're using an external tool If any of these files change, objdiff will automatically rebuild the objects and re-compare them. If not specified, objdiff will use the default patterns listed above. +`ignore_patterns` _(optional)_: A list of glob patterns to explicitly ignore when watching for changes. +([Supported syntax](https://docs.rs/globset/latest/globset/#syntax)) +If not specified, objdiff will use the default patterns listed above. + `units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. > `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. diff --git a/config.schema.json b/config.schema.json index 3b081d6..de97226 100644 --- a/config.schema.json +++ b/config.schema.json @@ -74,6 +74,16 @@ "*.json" ] }, + "ignore_patterns": { + "type": "array", + "description": "List of glob patterns to explicitly ignore when watching for changes.\nFiles matching these patterns will not trigger a rebuild.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax", + "items": { + "type": "string" + }, + "default": [ + "build/**/*" + ] + }, "objects": { "type": "array", "description": "Use units instead.", diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index ef6faac..f70509e 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -342,10 +342,12 @@ fn run_interactive( }; if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) { let watch_patterns = project_config.build_watch_patterns()?; + let ignore_patterns = project_config.build_ignore_patterns()?; state.watcher = Some(create_watcher( state.modified.clone(), project_dir.as_ref(), build_globset(&watch_patterns)?, + build_globset(&ignore_patterns)?, Waker::from(state.waker.clone()), )?); } diff --git a/objdiff-core/src/build/watcher.rs b/objdiff-core/src/build/watcher.rs index 65c00bb..2034fba 100644 --- a/objdiff-core/src/build/watcher.rs +++ b/objdiff-core/src/build/watcher.rs @@ -29,6 +29,7 @@ pub fn create_watcher( modified: Arc, project_dir: &Path, patterns: GlobSet, + ignore_patterns: GlobSet, waker: Waker, ) -> notify::Result { let base_dir = fs::canonicalize(project_dir)?; @@ -54,8 +55,8 @@ pub fn create_watcher( let Ok(path) = path.strip_prefix(&base_dir_clone) else { continue; }; - if patterns.is_match(path) { - // log::info!("File modified: {}", path.display()); + if patterns.is_match(path) && !ignore_patterns.is_match(path) { + log::info!("File modified: {}", path.display()); any_match = true; } } diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index d3f7986..bf953de 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -36,6 +36,8 @@ pub struct ProjectConfig { pub build_target: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub watch_patterns: Option>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub ignore_patterns: Option>, #[cfg_attr( feature = "serde", serde(alias = "objects", skip_serializing_if = "Option::is_none") @@ -66,7 +68,18 @@ impl ProjectConfig { .map(|s| Glob::new(s)) .collect::, globset::Error>>()? } else { - DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() + default_watch_patterns() + }) + } + + pub fn build_ignore_patterns(&self) -> Result, globset::Error> { + Ok(if let Some(ignore_patterns) = &self.ignore_patterns { + ignore_patterns + .iter() + .map(|s| Glob::new(s)) + .collect::, globset::Error>>()? + } else { + default_ignore_patterns() }) } } @@ -195,10 +208,16 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ "*.inc", "*.py", "*.yml", "*.txt", "*.json", ]; +pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"]; + pub fn default_watch_patterns() -> Vec { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() } +pub fn default_ignore_patterns() -> Vec { + DEFAULT_IGNORE_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() +} + #[cfg(feature = "std")] #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index 880df3c..b0552a5 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -16,8 +16,8 @@ use globset::Glob; use objdiff_core::{ build::watcher::{Watcher, create_watcher}, config::{ - DEFAULT_WATCH_PATTERNS, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, - build_globset, default_watch_patterns, path::platform_path_serde_option, + ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset, + default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option, save_project_config, }, diff::DiffObjConfig, @@ -219,6 +219,8 @@ pub struct AppConfig { #[serde(default = "default_watch_patterns")] pub watch_patterns: Vec, #[serde(default)] + pub ignore_patterns: Vec, + #[serde(default)] pub recent_projects: Vec, #[serde(default)] pub diff_obj_config: DiffObjConfig, @@ -239,7 +241,8 @@ impl Default for AppConfig { build_target: false, rebuild_on_changes: true, auto_update_check: true, - watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(), + watch_patterns: default_watch_patterns(), + ignore_patterns: default_ignore_patterns(), recent_projects: vec![], diff_obj_config: Default::default(), } @@ -560,11 +563,17 @@ impl App { if let Some(project_dir) = &state.config.project_dir { match build_globset(&state.config.watch_patterns) .map_err(anyhow::Error::new) - .and_then(|globset| { + .and_then(|patterns| { + build_globset(&state.config.ignore_patterns) + .map(|ignore_patterns| (patterns, ignore_patterns)) + .map_err(anyhow::Error::new) + }) + .and_then(|(patterns, ignore_patterns)| { create_watcher( self.modified.clone(), project_dir.as_ref(), - globset, + patterns, + ignore_patterns, egui_waker(ctx), ) .map_err(anyhow::Error::new) diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index 16583c1..ad25bc9 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -1,6 +1,6 @@ use anyhow::Result; use globset::Glob; -use objdiff_core::config::{DEFAULT_WATCH_PATTERNS, try_project_config}; +use objdiff_core::config::{default_ignore_patterns, default_watch_patterns, try_project_config}; use typed_path::{Utf8UnixComponent, Utf8UnixPath}; use crate::app::{AppState, ObjectConfig}; @@ -96,8 +96,15 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { .map(|s| Glob::new(s)) .collect::, globset::Error>>()?; } else { - state.config.watch_patterns = - DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); + state.config.watch_patterns = default_watch_patterns(); + } + if let Some(ignore_patterns) = &project_config.ignore_patterns { + state.config.ignore_patterns = ignore_patterns + .iter() + .map(|s| Glob::new(s)) + .collect::, globset::Error>>()?; + } else { + state.config.ignore_patterns = default_ignore_patterns(); } state.watcher_change = true; state.objects = project_config diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index d2050cb..32956d1 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -10,7 +10,7 @@ use egui::{ }; use globset::Glob; use objdiff_core::{ - config::{DEFAULT_WATCH_PATTERNS, path::check_path_buf}, + config::{default_ignore_patterns, default_watch_patterns, path::check_path_buf}, diff::{ CONFIG_GROUPS, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, ConfigPropertyValue, @@ -41,6 +41,7 @@ pub struct ConfigViewState { pub build_running: bool, pub queue_build: bool, pub watch_pattern_text: String, + pub ignore_pattern_text: String, pub object_search: String, pub filter_diffable: bool, pub filter_incomplete: bool, @@ -790,20 +791,49 @@ fn split_obj_config_ui( state.watcher_change = true; }; + state.watcher_change |= patterns_ui( + ui, + "File patterns", + &mut state.config.watch_patterns, + &mut config_state.watch_pattern_text, + appearance, + state.project_config_info.is_some(), + default_watch_patterns, + ); + state.watcher_change |= patterns_ui( + ui, + "Ignore patterns", + &mut state.config.ignore_patterns, + &mut config_state.ignore_pattern_text, + appearance, + state.project_config_info.is_some(), + default_ignore_patterns, + ); +} + +fn patterns_ui( + ui: &mut egui::Ui, + text: &str, + patterns: &mut Vec, + pattern_text: &mut String, + appearance: &Appearance, + has_project_config: bool, + on_reset: impl FnOnce() -> Vec, +) -> bool { + let mut change = false; ui.horizontal(|ui| { - ui.label(RichText::new("File patterns").color(appearance.text_color)); + ui.label(RichText::new(text).color(appearance.text_color)); if ui - .add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset")) + .add_enabled(!has_project_config, egui::Button::new("Reset")) .on_disabled_hover_text(CONFIG_DISABLED_TEXT) .clicked() { - state.config.watch_patterns = - DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); - state.watcher_change = true; + *patterns = on_reset(); + change = true; } }); let mut remove_at: Option = None; - for (idx, glob) in state.config.watch_patterns.iter().enumerate() { + for (idx, glob) in patterns.iter().enumerate() { ui.horizontal(|ui| { ui.label( RichText::new(glob.to_string()) @@ -811,7 +841,7 @@ fn split_obj_config_ui( .family(FontFamily::Monospace), ); if ui - .add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small()) + .add_enabled(!has_project_config, egui::Button::new("-").small()) .on_disabled_hover_text(CONFIG_DISABLED_TEXT) .clicked() { @@ -820,26 +850,27 @@ fn split_obj_config_ui( }); } if let Some(idx) = remove_at { - state.config.watch_patterns.remove(idx); - state.watcher_change = true; + patterns.remove(idx); + change = true; } ui.horizontal(|ui| { ui.add_enabled( - state.project_config_info.is_none(), - egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0), + !has_project_config, + egui::TextEdit::singleline(pattern_text).desired_width(100.0), ) .on_disabled_hover_text(CONFIG_DISABLED_TEXT); if ui - .add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small()) + .add_enabled(!has_project_config, egui::Button::new("+").small()) .on_disabled_hover_text(CONFIG_DISABLED_TEXT) .clicked() - && let Ok(glob) = Glob::new(&config_state.watch_pattern_text) + && let Ok(glob) = Glob::new(pattern_text) { - state.config.watch_patterns.push(glob); - state.watcher_change = true; - config_state.watch_pattern_text.clear(); + patterns.push(glob); + change = true; + pattern_text.clear(); } }); + change } pub fn arch_config_window( diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index dad8201..d701f15 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -142,6 +142,11 @@ impl DiffViewState { JobResult::ObjDiff(result) => { self.build = take(result); + // Clear reload flag so that we don't reload the view immediately + if let Ok(mut state) = state.write() { + state.queue_reload = false; + } + // TODO: where should this go? if let Some(result) = self.post_build_nav.take() { self.current_view = result.view;