diff --git a/crates/code2prompt-core/src/configuration.rs b/crates/code2prompt-core/src/configuration.rs index ec3682d..e9c9091 100644 --- a/crates/code2prompt-core/src/configuration.rs +++ b/crates/code2prompt-core/src/configuration.rs @@ -85,6 +85,9 @@ pub struct Code2PromptConfig { /// (parallelized during file I/O). This flag only controls whether the breakdown /// is shown to users in the final output. pub token_map_enabled: bool, + + /// If true, starts with all files deselected. + pub deselected: bool, } impl Code2PromptConfig { @@ -148,6 +151,9 @@ pub struct TomlConfig { /// Token map pub token_map_enabled: bool, + + /// Initial selection state + pub deselected: bool, } impl TomlConfig { @@ -208,7 +214,8 @@ impl TomlConfig { builder .user_variables(self.user_variables.clone()) - .token_map_enabled(self.token_map_enabled); + .token_map_enabled(self.token_map_enabled) + .deselected(self.deselected); builder.build().unwrap_or_default() } @@ -249,6 +256,7 @@ pub fn export_config_to_toml(config: &Code2PromptConfig) -> Result, + + /// Default behavior when no patterns or user actions match + deselected_by_default: bool, } impl SelectionEngine { /// Create a new SelectionEngine with base patterns - pub fn new(include_patterns: Vec, exclude_patterns: Vec) -> Self { + pub fn new( + include_patterns: Vec, + exclude_patterns: Vec, + deselected_by_default: bool, + ) -> Self { Self { filter_engine: FilterEngine::new(&include_patterns, &exclude_patterns), user_actions: Vec::new(), cache: HashMap::new(), + deselected_by_default, } } @@ -74,9 +82,12 @@ impl SelectionEngine { // If there are include patterns, use them self.filter_engine.matches_patterns(path) } else { - // No include patterns: default behavior is to include all files - // (unless excluded by exclude patterns) - !self.filter_engine.is_excluded(path) + // No include patterns: default behavior depends on deselected_by_default + if self.deselected_by_default { + false + } else { + !self.filter_engine.is_excluded(path) + } } } @@ -241,6 +252,12 @@ impl SelectionEngine { pub fn filter_engine(&self) -> &FilterEngine { &self.filter_engine } + + /// Set whether the engine should default to deselected + pub fn set_deselected_by_default(&mut self, value: bool) { + self.deselected_by_default = value; + self.cache.clear(); + } } impl std::fmt::Debug for SelectionEngine { @@ -249,6 +266,7 @@ impl std::fmt::Debug for SelectionEngine { .field("filter_engine", &self.filter_engine) .field("user_actions", &self.user_actions) .field("cache_size", &self.cache.len()) + .field("deselected_by_default", &self.deselected_by_default) .finish() } } @@ -259,7 +277,7 @@ mod tests { #[test] fn test_specificity_calculation() { - let engine = SelectionEngine::new(vec![], vec![]); + let engine = SelectionEngine::new(vec![], vec![], false); assert_eq!(engine.calculate_specificity(Path::new("file.rs")), 1); assert_eq!(engine.calculate_specificity(Path::new("src/main.rs")), 2); @@ -271,7 +289,7 @@ mod tests { #[test] fn test_precedence_rules() { - let mut engine = SelectionEngine::new(vec![], vec![]); + let mut engine = SelectionEngine::new(vec![], vec![], false); // Add less specific action first engine.exclude_file(PathBuf::from("src")); @@ -286,7 +304,7 @@ mod tests { #[test] fn test_recent_wins_over_old() { - let mut engine = SelectionEngine::new(vec![], vec![]); + let mut engine = SelectionEngine::new(vec![], vec![], false); // First action engine.exclude_file(PathBuf::from("main.rs")); @@ -296,4 +314,18 @@ mod tests { engine.include_file(PathBuf::from("main.rs")); assert!(engine.is_selected(Path::new("main.rs"))); } + + #[test] + fn test_deselected_by_default() { + let mut engine = SelectionEngine::new(vec![], vec![], true); + + // By default everything is deselected + assert!(!engine.is_selected(Path::new("main.rs"))); + assert!(!engine.is_selected(Path::new("src/lib.rs"))); + + // User action should still work + engine.include_file(PathBuf::from("main.rs")); + assert!(engine.is_selected(Path::new("main.rs"))); + assert!(!engine.is_selected(Path::new("src/lib.rs"))); + } } diff --git a/crates/code2prompt-core/src/session.rs b/crates/code2prompt-core/src/session.rs index fdc3e1b..10853d2 100644 --- a/crates/code2prompt-core/src/session.rs +++ b/crates/code2prompt-core/src/session.rs @@ -76,6 +76,7 @@ impl Code2PromptSession { let selection_engine = SelectionEngine::new( config.include_patterns.clone(), config.exclude_patterns.clone(), + config.deselected, ); Self { @@ -92,6 +93,7 @@ impl Code2PromptSession { self.selection_engine = SelectionEngine::new( self.config.include_patterns.clone(), self.config.exclude_patterns.clone(), + self.config.deselected, ); self } @@ -102,6 +104,7 @@ impl Code2PromptSession { self.selection_engine = SelectionEngine::new( self.config.include_patterns.clone(), self.config.exclude_patterns.clone(), + self.config.deselected, ); self } @@ -177,6 +180,13 @@ impl Code2PromptSession { self.selection_engine.has_user_actions() } + /// Set deselected by default and update selection engine + pub fn set_deselected(&mut self, value: bool) -> &mut Self { + self.config.deselected = value; + self.selection_engine.set_deselected_by_default(value); + self + } + /// Loads the codebase data (source tree and file list) into the session. pub fn load_codebase(&mut self) -> Result<()> { let (tree, files) = traverse_directory(&self.config, Some(&mut self.selection_engine)) diff --git a/crates/code2prompt/src/args.rs b/crates/code2prompt/src/args.rs index 4ba83fc..c53b246 100644 --- a/crates/code2prompt/src/args.rs +++ b/crates/code2prompt/src/args.rs @@ -142,6 +142,10 @@ pub struct Cli { #[clap(long, value_name = "PERCENT")] pub token_map_min_percent: Option, + /// Start with all files deselected + #[clap(long)] + pub deselected: bool, + #[arg(long, hide = true)] pub clipboard_daemon: bool, } diff --git a/crates/code2prompt/src/config.rs b/crates/code2prompt/src/config.rs index e620480..c1345df 100644 --- a/crates/code2prompt/src/config.rs +++ b/crates/code2prompt/src/config.rs @@ -162,6 +162,7 @@ pub fn build_session( let cfg_diff_enabled = cfg.map(|c| c.diff_enabled).unwrap_or(false); let cfg_token_map_enabled = cfg.map(|c| c.token_map_enabled).unwrap_or(false); + let cfg_deselected = cfg.map(|c| c.deselected).unwrap_or(false); configuration .diff_enabled(args.diff || cfg_diff_enabled) @@ -171,7 +172,8 @@ pub fn build_session( .hidden(args.hidden) .no_codeblock(args.no_codeblock) .follow_symlinks(args.follow_symlinks) - .token_map_enabled(args.token_map || cfg_token_map_enabled || tui_mode); + .token_map_enabled(args.token_map || cfg_token_map_enabled || tui_mode) + .deselected(args.deselected || cfg_deselected); // User variables from config (if available) if let Some(c) = cfg { diff --git a/crates/code2prompt/src/model/settings.rs b/crates/code2prompt/src/model/settings.rs index 6748757..da07269 100644 --- a/crates/code2prompt/src/model/settings.rs +++ b/crates/code2prompt/src/model/settings.rs @@ -59,6 +59,7 @@ pub enum SettingKey { FollowSymlinks, HiddenFiles, NoIgnore, + Deselected, } impl SettingsState { @@ -162,6 +163,10 @@ impl SettingsState { session.config.no_ignore = !session.config.no_ignore; "No Ignore" } + (SettingKey::Deselected, SettingAction::Toggle | SettingAction::Cycle) => { + session.set_deselected(!session.config.deselected); + "Deselected by Default" + } _ => "Unknown Setting", } } diff --git a/crates/code2prompt/src/view/formatters.rs b/crates/code2prompt/src/view/formatters.rs index 665ced8..899d052 100644 --- a/crates/code2prompt/src/view/formatters.rs +++ b/crates/code2prompt/src/view/formatters.rs @@ -151,6 +151,12 @@ pub fn format_settings_groups(session: &Code2PromptSession) -> Vec