@@ -6,7 +6,7 @@ use crate::output::{
66use crate :: source_detection;
77use crate :: tokenizer:: TokenCounter ;
88use anyhow:: Result ;
9- use ignore:: WalkBuilder ;
9+ use ignore:: { overrides :: OverrideBuilder , WalkBuilder } ;
1010use indicatif:: { ProgressBar , ProgressStyle } ;
1111use rayon:: prelude:: * ;
1212use std:: fs;
@@ -29,28 +29,35 @@ pub fn process_directory(args: &Cli) -> Result<()> {
2929 ) ;
3030 pb. set_message ( "Scanning files..." ) ;
3131
32- let max_size = args. max_size . expect ( "max_size should be set from config" ) ;
33- let max_depth = args. max_depth . expect ( "max_depth should be set from config" ) ;
3432 let output_format = args
3533 . output
3634 . clone ( )
3735 . expect ( "output format should be set from config" ) ;
36+ let entries = process_entries ( & args) ?;
37+ pb. finish ( ) ;
3838
39- // Build the walker with ignore patterns
40- let mut builder = WalkBuilder :: new ( & args. paths [ 0 ] ) ;
41- builder
42- . max_depth ( Some ( max_depth) )
43- . hidden ( !args. hidden )
44- . git_ignore ( !args. no_ignore )
45- . ignore ( !args. no_ignore ) ;
46-
47- if let Some ( ref includes) = args. include {
48- for pattern in includes {
49- builder. add_custom_ignore_filename ( pattern) ;
50- }
39+ if let Some ( pdf_path) = & args. pdf {
40+ let pdf_data = generate_pdf ( & entries, args. output . clone ( ) . unwrap_or ( OutputFormat :: Both ) ) ?;
41+ fs:: write ( pdf_path, pdf_data) ?;
42+ println ! ( "PDF output written to: {}" , pdf_path. display( ) ) ;
43+ } else {
44+ // Handle output (print/copy/save)
45+ let output = generate_output ( & entries, output_format) ?;
46+ handle_output ( output, args) ?;
5147 }
5248
53- // Collect all valid files
49+ if !args. no_tokens {
50+ let counter = create_token_counter ( args) ?;
51+ display_token_counts ( counter, & entries) ?;
52+ }
53+
54+ Ok ( ( ) )
55+ }
56+
57+ pub fn process_entries ( args : & Cli ) -> Result < Vec < FileEntry > > {
58+ let max_size = args. max_size . expect ( "max_size should be set from config" ) ;
59+ let max_depth = args. max_depth . expect ( "max_depth should be set from config" ) ;
60+
5461 let entries = if args. interactive {
5562 let mut picker = FilePicker :: new (
5663 PathBuf :: from ( & args. paths [ 0 ] ) ,
@@ -73,8 +80,8 @@ pub fn process_directory(args: &Cli) -> Result<()> {
7380 . collect :: < Vec < FileEntry > > ( )
7481 } else {
7582 let mut all_entries = Vec :: new ( ) ;
76- for path in & args. paths {
77- let path = std:: path:: Path :: new ( path ) ;
83+ for path_str in & args. paths {
84+ let path = std:: path:: Path :: new ( path_str ) ;
7885 if path. is_dir ( ) {
7986 let mut builder = WalkBuilder :: new ( path) ;
8087 builder
@@ -89,14 +96,67 @@ pub fn process_directory(args: &Cli) -> Result<()> {
8996 }
9097 }
9198
99+ let mut override_builder = OverrideBuilder :: new ( path) ;
100+ if let Some ( ref excludes) = args. exclude {
101+ for exclude in excludes {
102+ match exclude {
103+ Exclude :: Pattern ( pattern) => {
104+ // Add a '!' prefix if it doesn't already have one
105+ // This makes it a negative pattern (exclude)
106+ let exclude_pattern = if !pattern. starts_with ( '!' ) {
107+ format ! ( "!{}" , pattern)
108+ } else {
109+ pattern. clone ( )
110+ } ;
111+
112+ if let Err ( e) = override_builder. add ( & exclude_pattern) {
113+ eprintln ! (
114+ "Warning: Invalid exclude pattern '{}': {}" ,
115+ pattern, e
116+ ) ;
117+ }
118+ }
119+ Exclude :: File ( file_path) => {
120+ // For file excludes, handle differently if:
121+ if file_path. is_absolute ( ) {
122+ // For absolute paths, check if they exist
123+ if file_path. exists ( ) {
124+ // If base_path is part of file_path, make it relative
125+ if let Ok ( relative_path) = file_path. strip_prefix ( path) {
126+ let pattern = format ! ( "!{}" , relative_path. display( ) ) ;
127+ if let Err ( e) = override_builder. add ( & pattern) {
128+ eprintln ! ( "Warning: Could not add file exclude pattern for '{}': {}" , file_path. display( ) , e) ;
129+ }
130+ } else {
131+ // This doesn't affect current directory
132+ eprintln ! (
133+ "Note: File exclude not under current path: {}" ,
134+ file_path. display( )
135+ ) ;
136+ }
137+ }
138+ } else {
139+ // For relative paths like "src", use as-is with a ! prefix
140+ let pattern = format ! ( "!{}" , file_path. display( ) ) ;
141+ if let Err ( e) = override_builder. add ( & pattern) {
142+ eprintln ! (
143+ "Warning: Could not add file exclude pattern '{}': {}" ,
144+ pattern, e
145+ ) ;
146+ }
147+ }
148+ }
149+ }
150+ }
151+ }
152+ let overrides = override_builder. build ( ) ?;
153+ builder. overrides ( overrides) ;
154+
92155 let dir_entries: Vec < FileEntry > = builder
93156 . build ( )
94157 . par_bridge ( )
95158 . filter_map ( |entry| entry. ok ( ) )
96- . filter ( |entry| {
97- let should_exclude = is_excluded ( entry, args) ;
98- !should_exclude
99- } )
159+ // No longer need the is_excluded filter here, WalkBuilder handles it
100160 . filter ( |entry| {
101161 entry. file_type ( ) . map ( |ft| ft. is_file ( ) ) . unwrap_or ( false )
102162 && source_detection:: is_source_file ( entry. path ( ) )
@@ -117,12 +177,44 @@ pub fn process_directory(args: &Cli) -> Result<()> {
117177 . map ( |m| m. len ( ) <= max_size)
118178 . unwrap_or ( false )
119179 {
120- let entry = ignore:: WalkBuilder :: new ( path)
121- . build ( )
122- . next ( )
123- . and_then ( |r| r. ok ( ) ) ;
124- if let Some ( entry) = entry {
125- if !is_excluded ( & entry, args) {
180+ // Need to check excludes even for single files explicitly passed
181+ let mut excluded = false ;
182+ if let Some ( ref excludes) = args. exclude {
183+ let mut override_builder =
184+ OverrideBuilder :: new ( path. parent ( ) . unwrap_or ( path) ) ; // Base relative to parent
185+ for exclude in excludes {
186+ match exclude {
187+ Exclude :: Pattern ( pattern) => {
188+ if let Err ( e) = override_builder. add ( pattern) {
189+ eprintln ! (
190+ "Warning: Invalid exclude pattern '{}': {}" ,
191+ pattern, e
192+ ) ;
193+ }
194+ }
195+ Exclude :: File ( file_path) => {
196+ if path == file_path {
197+ excluded = true ;
198+ break ;
199+ }
200+ }
201+ }
202+ }
203+ if excluded {
204+ continue ;
205+ }
206+ let overrides = override_builder. build ( ) ?;
207+ if overrides. matched ( path, false ) . is_ignore ( ) {
208+ excluded = true ;
209+ }
210+ }
211+
212+ if !excluded {
213+ let entry = ignore:: WalkBuilder :: new ( path)
214+ . build ( )
215+ . next ( )
216+ . and_then ( |r| r. ok ( ) ) ;
217+ if let Some ( entry) = entry {
126218 if let Ok ( file_entry) = process_file ( & entry, path) {
127219 all_entries. push ( file_entry) ;
128220 }
@@ -133,66 +225,11 @@ pub fn process_directory(args: &Cli) -> Result<()> {
133225 }
134226 all_entries
135227 } ;
136- pb. finish ( ) ;
137-
138- if let Some ( pdf_path) = & args. pdf {
139- let pdf_data = generate_pdf ( & entries, args. output . clone ( ) . unwrap_or ( OutputFormat :: Both ) ) ?;
140- fs:: write ( pdf_path, pdf_data) ?;
141- println ! ( "PDF output written to: {}" , pdf_path. display( ) ) ;
142- } else {
143- // Handle output (print/copy/save)
144- let output = generate_output ( & entries, output_format) ?;
145- handle_output ( output, args) ?;
146- }
147-
148- if !args. no_tokens {
149- let counter = create_token_counter ( args) ?;
150- display_token_counts ( counter, & entries) ?;
151- }
152228
153- Ok ( ( ) )
229+ Ok ( entries )
154230}
155231
156- fn is_excluded ( entry : & ignore:: DirEntry , args : & Cli ) -> bool {
157- if let Some ( excludes) = & args. exclude {
158- let path_str = entry. path ( ) . to_string_lossy ( ) ;
159-
160- for exclude in excludes {
161- match exclude {
162- Exclude :: Pattern ( pattern) => {
163- let pattern = if !pattern. starts_with ( "./" ) && !pattern. starts_with ( "/" ) {
164- format ! ( "./{}" , pattern)
165- } else {
166- pattern. clone ( )
167- } ;
168-
169- if let Ok ( glob) = globset:: GlobBuilder :: new ( & pattern)
170- . case_insensitive ( false )
171- . build ( )
172- {
173- let matcher = glob. compile_matcher ( ) ;
174- let check_path = if !path_str. starts_with ( "./" ) {
175- format ! ( "./{}" , path_str)
176- } else {
177- path_str. to_string ( )
178- } ;
179-
180- if matcher. is_match ( & check_path) {
181- return true ;
182- }
183- }
184- }
185- Exclude :: File ( path) => {
186- let matches = entry. path ( ) . ends_with ( path) ;
187- if matches {
188- return true ;
189- }
190- }
191- }
192- }
193- }
194- false
195- }
232+ // Removed the is_excluded function as it's now handled by WalkBuilder overrides
196233
197234pub fn create_token_counter ( args : & Cli ) -> Result < TokenCounter > {
198235 match args. tokenizer . as_ref ( ) . unwrap_or ( & TokenizerType :: Tiktoken ) {
@@ -296,35 +333,61 @@ mod tests {
296333 }
297334
298335 #[ test]
299- fn test_exclude_patterns ( ) {
300- let ( dir, _files) = setup_test_directory ( ) . unwrap ( ) ;
301- let entry = ignore:: WalkBuilder :: new ( dir. path ( ) )
302- . build ( )
303- . find ( |e| e. as_ref ( ) . unwrap ( ) . path ( ) . ends_with ( "main.rs" ) )
304- . unwrap ( )
305- . unwrap ( ) ;
306-
307- // Test various exclude patterns
336+ fn test_exclude_patterns ( ) -> Result < ( ) > {
337+ let ( dir, _files) = setup_test_directory ( ) ?;
338+
339+ let main_rs_path = dir. path ( ) . join ( "src/main.rs" ) ;
340+
308341 let test_cases = vec ! [
309342 // Pattern exclusions
310343 ( Exclude :: Pattern ( "**/*.rs" . to_string( ) ) , true ) ,
311344 ( Exclude :: Pattern ( "**/*.js" . to_string( ) ) , false ) ,
312345 ( Exclude :: Pattern ( "test/**" . to_string( ) ) , false ) ,
313346 // File exclusions
314- ( Exclude :: File ( PathBuf :: from ( "main.rs" ) ) , true ) ,
347+ ( Exclude :: File ( main_rs_path . clone ( ) ) , true ) ,
315348 ( Exclude :: File ( PathBuf :: from( "nonexistent.rs" ) ) , false ) ,
316349 ] ;
317350
318351 for ( exclude, should_exclude) in test_cases {
319- let mut cli = create_test_cli ( dir. path ( ) ) ;
320- cli. exclude = Some ( vec ! [ exclude] ) ;
352+ let mut override_builder = OverrideBuilder :: new ( dir. path ( ) ) ;
353+
354+ match & exclude {
355+ Exclude :: Pattern ( pattern) => {
356+ // For patterns that should exclude, we need to add a "!" prefix
357+ // to make them negative patterns (exclusions)
358+ let exclude_pattern = if !pattern. starts_with ( '!' ) {
359+ format ! ( "!{}" , pattern)
360+ } else {
361+ pattern. clone ( )
362+ } ;
363+ override_builder. add ( & exclude_pattern) . unwrap ( ) ;
364+ }
365+ Exclude :: File ( file_path) => {
366+ if file_path. exists ( ) {
367+ // Get the file path relative to the test directory
368+ let rel_path = if file_path. is_absolute ( ) {
369+ file_path. strip_prefix ( dir. path ( ) ) . unwrap_or ( file_path)
370+ } else {
371+ file_path
372+ } ;
373+ // Add as a negative pattern
374+ let pattern = format ! ( "!{}" , rel_path. display( ) ) ;
375+ override_builder. add ( & pattern) . unwrap ( ) ;
376+ }
377+ }
378+ }
379+
380+ let overrides = override_builder. build ( ) ?;
381+ let is_ignored = overrides. matched ( & main_rs_path, false ) . is_ignore ( ) ;
382+
321383 assert_eq ! (
322- is_excluded( & entry, & cli) ,
323- should_exclude,
324- "Failed for exclude pattern: {:?}" ,
325- cli. exclude
384+ is_ignored, should_exclude,
385+ "Failed for exclude: {:?}" ,
386+ exclude
326387 ) ;
327388 }
389+
390+ Ok ( ( ) )
328391 }
329392
330393 #[ test]
0 commit comments