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
This commit is contained in:
Luke Street 2025-08-15 16:24:26 -06:00
parent 813c8aa539
commit 52c138bf06
9 changed files with 119 additions and 28 deletions

View File

@ -106,6 +106,9 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
"*.txt", "*.txt",
"*.json" "*.json"
], ],
"ignore_patterns": [
"build/**/*"
],
"units": [ "units": [
{ {
"name": "main/MetroTRK/mslsupp", "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 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. 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. `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. > `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.

View File

@ -74,6 +74,16 @@
"*.json" "*.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": { "objects": {
"type": "array", "type": "array",
"description": "Use units instead.", "description": "Use units instead.",

View File

@ -342,10 +342,12 @@ fn run_interactive(
}; };
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) { if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
let watch_patterns = project_config.build_watch_patterns()?; let watch_patterns = project_config.build_watch_patterns()?;
let ignore_patterns = project_config.build_ignore_patterns()?;
state.watcher = Some(create_watcher( state.watcher = Some(create_watcher(
state.modified.clone(), state.modified.clone(),
project_dir.as_ref(), project_dir.as_ref(),
build_globset(&watch_patterns)?, build_globset(&watch_patterns)?,
build_globset(&ignore_patterns)?,
Waker::from(state.waker.clone()), Waker::from(state.waker.clone()),
)?); )?);
} }

View File

@ -29,6 +29,7 @@ pub fn create_watcher(
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
project_dir: &Path, project_dir: &Path,
patterns: GlobSet, patterns: GlobSet,
ignore_patterns: GlobSet,
waker: Waker, waker: Waker,
) -> notify::Result<Watcher> { ) -> notify::Result<Watcher> {
let base_dir = fs::canonicalize(project_dir)?; 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 { let Ok(path) = path.strip_prefix(&base_dir_clone) else {
continue; continue;
}; };
if patterns.is_match(path) { if patterns.is_match(path) && !ignore_patterns.is_match(path) {
// log::info!("File modified: {}", path.display()); log::info!("File modified: {}", path.display());
any_match = true; any_match = true;
} }
} }

View File

@ -36,6 +36,8 @@ pub struct ProjectConfig {
pub build_target: Option<bool>, pub build_target: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub watch_patterns: Option<Vec<String>>, pub watch_patterns: Option<Vec<String>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub ignore_patterns: Option<Vec<String>>,
#[cfg_attr( #[cfg_attr(
feature = "serde", feature = "serde",
serde(alias = "objects", skip_serializing_if = "Option::is_none") serde(alias = "objects", skip_serializing_if = "Option::is_none")
@ -66,7 +68,18 @@ impl ProjectConfig {
.map(|s| Glob::new(s)) .map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()? .collect::<Result<Vec<Glob>, globset::Error>>()?
} else { } else {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() default_watch_patterns()
})
}
pub fn build_ignore_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
Ok(if let Some(ignore_patterns) = &self.ignore_patterns {
ignore_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?
} else {
default_ignore_patterns()
}) })
} }
} }
@ -195,10 +208,16 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.inc", "*.py", "*.yml", "*.txt", "*.json", "*.inc", "*.py", "*.yml", "*.txt", "*.json",
]; ];
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"];
pub fn default_watch_patterns() -> Vec<Glob> { pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
} }
pub fn default_ignore_patterns() -> Vec<Glob> {
DEFAULT_IGNORE_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {

View File

@ -16,8 +16,8 @@ use globset::Glob;
use objdiff_core::{ use objdiff_core::{
build::watcher::{Watcher, create_watcher}, build::watcher::{Watcher, create_watcher},
config::{ config::{
DEFAULT_WATCH_PATTERNS, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset,
build_globset, default_watch_patterns, path::platform_path_serde_option, default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option,
save_project_config, save_project_config,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
@ -219,6 +219,8 @@ pub struct AppConfig {
#[serde(default = "default_watch_patterns")] #[serde(default = "default_watch_patterns")]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)] #[serde(default)]
pub ignore_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<String>, pub recent_projects: Vec<String>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
@ -239,7 +241,8 @@ impl Default for AppConfig {
build_target: false, build_target: false,
rebuild_on_changes: true, rebuild_on_changes: true,
auto_update_check: 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![], recent_projects: vec![],
diff_obj_config: Default::default(), diff_obj_config: Default::default(),
} }
@ -560,11 +563,17 @@ impl App {
if let Some(project_dir) = &state.config.project_dir { if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns) match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new) .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( create_watcher(
self.modified.clone(), self.modified.clone(),
project_dir.as_ref(), project_dir.as_ref(),
globset, patterns,
ignore_patterns,
egui_waker(ctx), egui_waker(ctx),
) )
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use globset::Glob; 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 typed_path::{Utf8UnixComponent, Utf8UnixPath};
use crate::app::{AppState, ObjectConfig}; use crate::app::{AppState, ObjectConfig};
@ -96,8 +96,15 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
.map(|s| Glob::new(s)) .map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?; .collect::<Result<Vec<Glob>, globset::Error>>()?;
} else { } else {
state.config.watch_patterns = state.config.watch_patterns = default_watch_patterns();
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); }
if let Some(ignore_patterns) = &project_config.ignore_patterns {
state.config.ignore_patterns = ignore_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?;
} else {
state.config.ignore_patterns = default_ignore_patterns();
} }
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config state.objects = project_config

View File

@ -10,7 +10,7 @@ use egui::{
}; };
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{DEFAULT_WATCH_PATTERNS, path::check_path_buf}, config::{default_ignore_patterns, default_watch_patterns, path::check_path_buf},
diff::{ diff::{
CONFIG_GROUPS, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, CONFIG_GROUPS, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
ConfigPropertyValue, ConfigPropertyValue,
@ -41,6 +41,7 @@ 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 ignore_pattern_text: 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,
@ -790,20 +791,49 @@ fn split_obj_config_ui(
state.watcher_change = true; 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<Glob>,
pattern_text: &mut String,
appearance: &Appearance,
has_project_config: bool,
on_reset: impl FnOnce() -> Vec<Glob>,
) -> bool {
let mut change = false;
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color)); ui.label(RichText::new(text).color(appearance.text_color));
if ui 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) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
state.config.watch_patterns = *patterns = on_reset();
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); change = true;
state.watcher_change = true;
} }
}); });
let mut remove_at: Option<usize> = None; let mut remove_at: Option<usize> = None;
for (idx, glob) in state.config.watch_patterns.iter().enumerate() { for (idx, glob) in patterns.iter().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(glob.to_string()) RichText::new(glob.to_string())
@ -811,7 +841,7 @@ fn split_obj_config_ui(
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
if ui 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) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
@ -820,26 +850,27 @@ fn split_obj_config_ui(
}); });
} }
if let Some(idx) = remove_at { if let Some(idx) = remove_at {
state.config.watch_patterns.remove(idx); patterns.remove(idx);
state.watcher_change = true; change = true;
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_enabled( ui.add_enabled(
state.project_config_info.is_none(), !has_project_config,
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0), egui::TextEdit::singleline(pattern_text).desired_width(100.0),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT); .on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui 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) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
&& let Ok(glob) = Glob::new(&config_state.watch_pattern_text) && let Ok(glob) = Glob::new(pattern_text)
{ {
state.config.watch_patterns.push(glob); patterns.push(glob);
state.watcher_change = true; change = true;
config_state.watch_pattern_text.clear(); pattern_text.clear();
} }
}); });
change
} }
pub fn arch_config_window( pub fn arch_config_window(

View File

@ -142,6 +142,11 @@ impl DiffViewState {
JobResult::ObjDiff(result) => { JobResult::ObjDiff(result) => {
self.build = take(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? // TODO: where should this go?
if let Some(result) = self.post_build_nav.take() { if let Some(result) = self.post_build_nav.take() {
self.current_view = result.view; self.current_view = result.view;