@@ -56,7 +56,6 @@ pub fn fs_list_dir(path: String) -> Result<Vec<FileNode>, String> {
5656 . map_err ( |e| e. to_string ( ) ) ?
5757 . filter_map ( |e| e. ok ( ) )
5858 . filter ( |e| {
59- // hide hidden files and node_modules/target
6059 let name = e. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
6160 !name. starts_with ( '.' ) && name != "node_modules" && name != "target"
6261 } )
@@ -86,4 +85,113 @@ pub fn fs_list_dir(path: String) -> Result<Vec<FileNode>, String> {
8685
8786 entries. sort_by ( |a, b| b. is_dir . cmp ( & a. is_dir ) . then ( a. name . cmp ( & b. name ) ) ) ;
8887 Ok ( entries)
88+ }
89+
90+ // ── NEW: write file ───────────────────────────────────────────────────────────
91+ #[ tauri:: command]
92+ pub fn fs_write_file ( path : String , content : String ) -> Result < ( ) , String > {
93+ std:: fs:: write ( & path, content) . map_err ( |e| e. to_string ( ) )
94+ }
95+
96+ // ── NEW: search in files ──────────────────────────────────────────────────────
97+ #[ derive( serde:: Serialize , Clone ) ]
98+ pub struct SearchMatch {
99+ pub path : String ,
100+ pub line : usize ,
101+ pub col_start : usize ,
102+ pub col_end : usize ,
103+ pub text : String ,
104+ }
105+
106+ #[ tauri:: command]
107+ pub fn fs_search_in_files (
108+ root : String ,
109+ query : String ,
110+ case_sensitive : bool ,
111+ use_regex : bool ,
112+ include_exts : Vec < String > , // e.g. ["ts","rs"] — empty = all
113+ exclude_dirs : Vec < String > , // e.g. ["node_modules",".git","target"]
114+ ) -> Result < Vec < SearchMatch > , String > {
115+ use std:: io:: { BufRead , BufReader } ;
116+
117+ if query. is_empty ( ) {
118+ return Ok ( vec ! [ ] ) ;
119+ }
120+
121+ // Build regex or plain pattern
122+ let pattern: Box < dyn Fn ( & str ) -> Option < ( usize , usize ) > + Send > = if use_regex {
123+ let flags = if case_sensitive { "" } else { "(?i)" } ;
124+ let re = regex:: Regex :: new ( & format ! ( "{}{}" , flags, & query) )
125+ . map_err ( |e| e. to_string ( ) ) ?;
126+ Box :: new ( move |line : & str | {
127+ re. find ( line) . map ( |m| ( m. start ( ) , m. end ( ) ) )
128+ } )
129+ } else {
130+ let needle = if case_sensitive { query. clone ( ) } else { query. to_lowercase ( ) } ;
131+ Box :: new ( move |line : & str | {
132+ let hay = if case_sensitive { line. to_string ( ) } else { line. to_lowercase ( ) } ;
133+ hay. find ( & needle) . map ( |s| ( s, s + needle. len ( ) ) )
134+ } )
135+ } ;
136+
137+ let default_excludes = [ "node_modules" , ".git" , "target" , "dist" , ".next" , "out" ] ;
138+ let mut results: Vec < SearchMatch > = Vec :: new ( ) ;
139+
140+ for entry in walkdir:: WalkDir :: new ( & root)
141+ . follow_links ( false )
142+ . into_iter ( )
143+ . filter_entry ( |e| {
144+ let name = e. file_name ( ) . to_string_lossy ( ) ;
145+ // skip hidden
146+ if name. starts_with ( '.' ) { return false ; }
147+ // skip default + user-supplied exclude dirs
148+ if e. file_type ( ) . is_dir ( ) {
149+ if default_excludes. contains ( & name. as_ref ( ) ) { return false ; }
150+ if exclude_dirs. iter ( ) . any ( |x| x == name. as_ref ( ) ) { return false ; }
151+ }
152+ true
153+ } )
154+ {
155+ let entry = match entry { Ok ( e) => e, Err ( _) => continue } ;
156+ if entry. file_type ( ) . is_dir ( ) { continue ; }
157+
158+ let path_str = entry. path ( ) . to_string_lossy ( ) . to_string ( ) ;
159+
160+ // extension filter
161+ if !include_exts. is_empty ( ) {
162+ let ext = entry. path ( )
163+ . extension ( )
164+ . and_then ( |e| e. to_str ( ) )
165+ . unwrap_or ( "" )
166+ . to_lowercase ( ) ;
167+ if !include_exts. iter ( ) . any ( |x| x. to_lowercase ( ) == ext) {
168+ continue ;
169+ }
170+ }
171+
172+ // skip large files (> 2 MB)
173+ if entry. metadata ( ) . map ( |m| m. len ( ) ) . unwrap_or ( 0 ) > 2_097_152 {
174+ continue ;
175+ }
176+
177+ let file = match std:: fs:: File :: open ( entry. path ( ) ) {
178+ Ok ( f) => f, Err ( _) => continue ,
179+ } ;
180+
181+ for ( i, line) in BufReader :: new ( file) . lines ( ) . enumerate ( ) {
182+ let line = match line { Ok ( l) => l, Err ( _) => break } ;
183+ if let Some ( ( cs, ce) ) = pattern ( & line) {
184+ results. push ( SearchMatch {
185+ path : path_str. clone ( ) ,
186+ line : i + 1 ,
187+ col_start : cs,
188+ col_end : ce,
189+ text : line. trim_end ( ) . to_string ( ) ,
190+ } ) ;
191+ if results. len ( ) >= 2000 { return Ok ( results) ; }
192+ }
193+ }
194+ }
195+
196+ Ok ( results)
89197}
0 commit comments