@@ -157,6 +157,11 @@ impl WorkspaceManager {
157157 tracing:: warn!( "Failed to start file watching: {}" , e) ;
158158 }
159159
160+ // Auto-open representative files to enable workspace symbol search out-of-the-box
161+ if let Err ( e) = self . auto_open_representative_files ( ) . await {
162+ tracing:: warn!( "Failed to auto-open representative files: {}" , e) ;
163+ }
164+
160165 Ok ( ( ) )
161166 }
162167
@@ -505,6 +510,148 @@ impl WorkspaceManager {
505510 tracing:: info!( "🔍 File watching started for languages: {:?}" , detected_languages) ;
506511 Ok ( ( ) )
507512 }
513+
514+ /// Automatically open representative files for each language to enable workspace symbol search
515+ async fn auto_open_representative_files ( & mut self ) -> Result < ( ) > {
516+ let detected_languages = self . get_detected_languages ( ) ?;
517+
518+ for language in detected_languages {
519+ if let Some ( file_path) = self . find_representative_file ( & language) . await ? {
520+ tracing:: debug!( "Auto-opening representative file for {}: {:?}" , language, file_path) ;
521+
522+ // Read file content
523+ if let Ok ( content) = std:: fs:: read_to_string ( & file_path) {
524+ // Check if file is already opened
525+ if self . is_file_opened ( & file_path) {
526+ continue ;
527+ }
528+
529+ // Determine language ID from file extension
530+ let language_id = if let Some ( ext) = file_path. extension ( ) . and_then ( |ext| ext. to_str ( ) ) {
531+ self . config_manager . get_language_for_extension ( ext)
532+ . unwrap_or_else ( || "plaintext" . to_string ( ) )
533+ } else {
534+ "plaintext" . to_string ( )
535+ } ;
536+
537+ // Get LSP client for this file
538+ if let Ok ( Some ( client) ) = self . get_client_for_file ( & file_path) . await {
539+ // Create LSP parameters for opening the file
540+ let uri = url:: Url :: from_file_path ( & file_path)
541+ . map_err ( |_| anyhow:: anyhow!( "Invalid file path: {:?}" , file_path) ) ?;
542+
543+ let params = lsp_types:: DidOpenTextDocumentParams {
544+ text_document : lsp_types:: TextDocumentItem {
545+ uri,
546+ language_id,
547+ version : 1 ,
548+ text : content,
549+ } ,
550+ } ;
551+
552+ // Open the file in the LSP client
553+ if let Err ( e) = client. did_open ( params) . await {
554+ tracing:: warn!( "Failed to auto-open file {:?} for {}: {}" , file_path, language, e) ;
555+ } else {
556+ tracing:: debug!( "Successfully auto-opened file for {} workspace indexing" , language) ;
557+
558+ // Mark file as opened
559+ self . opened_files . insert ( file_path. clone ( ) , FileState {
560+ version : 1 ,
561+ is_open : true ,
562+ } ) ;
563+ }
564+ }
565+ }
566+ }
567+ }
568+
569+ Ok ( ( ) )
570+ }
571+
572+ /// Find a representative file for the given language to trigger workspace indexing
573+ async fn find_representative_file ( & self , language : & str ) -> Result < Option < PathBuf > > {
574+ let extensions = self . config_manager . get_extensions_for_language ( language) ;
575+
576+ // Search for files with matching extensions in the workspace
577+ for extension in extensions {
578+ if let Some ( file_path) = self . find_first_file_with_extension ( & extension) . await ? {
579+ return Ok ( Some ( file_path) ) ;
580+ }
581+ }
582+
583+ Ok ( None )
584+ }
585+
586+ /// Find the first file with the given extension in the workspace, respecting exclude patterns
587+ async fn find_first_file_with_extension ( & self , extension : & str ) -> Result < Option < PathBuf > > {
588+ use std:: fs;
589+
590+ // Get all exclude patterns from config
591+ let exclude_patterns = self . config_manager . get_all_exclude_patterns ( ) ;
592+
593+ fn should_exclude_path ( path : & Path , exclude_patterns : & [ String ] ) -> bool {
594+ let path_str = path. to_string_lossy ( ) ;
595+
596+ // Check against exclude patterns (simple glob-like matching)
597+ for pattern in exclude_patterns {
598+ if pattern. starts_with ( "**/" ) && pattern. ends_with ( "/**" ) {
599+ // Pattern like "**/node_modules/**"
600+ let dir_name = & pattern[ 3 ..pattern. len ( ) -3 ] ;
601+ if path_str. contains ( & format ! ( "/{}/" , dir_name) ) ||
602+ path_str. ends_with ( & format ! ( "/{}" , dir_name) ) {
603+ return true ;
604+ }
605+ } else if pattern. starts_with ( "**/" ) {
606+ // Pattern like "**/dist"
607+ let suffix = & pattern[ 3 ..] ;
608+ if path_str. ends_with ( suffix) {
609+ return true ;
610+ }
611+ }
612+ }
613+
614+ // Also exclude hidden directories
615+ if let Some ( name) = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) {
616+ if name. starts_with ( '.' ) {
617+ return true ;
618+ }
619+ }
620+
621+ false
622+ }
623+
624+ fn find_file_recursive ( dir : & Path , extension : & str , exclude_patterns : & [ String ] ) -> Option < PathBuf > {
625+ if should_exclude_path ( dir, exclude_patterns) {
626+ return None ;
627+ }
628+
629+ if let Ok ( entries) = fs:: read_dir ( dir) {
630+ for entry in entries. flatten ( ) {
631+ let path = entry. path ( ) ;
632+
633+ if should_exclude_path ( & path, exclude_patterns) {
634+ continue ;
635+ }
636+
637+ if path. is_file ( ) {
638+ if let Some ( file_ext) = path. extension ( ) . and_then ( |e| e. to_str ( ) ) {
639+ if file_ext == extension {
640+ return Some ( path) ;
641+ }
642+ }
643+ } else if path. is_dir ( ) {
644+ if let Some ( found) = find_file_recursive ( & path, extension, exclude_patterns) {
645+ return Some ( found) ;
646+ }
647+ }
648+ }
649+ }
650+ None
651+ }
652+
653+ Ok ( find_file_recursive ( & self . workspace_root , extension, & exclude_patterns) )
654+ }
508655}
509656
510657#[ cfg( test) ]
0 commit comments