@@ -157,10 +157,20 @@ interface CelCommandRegistry {
157157 handler : ( ...args : unknown [ ] ) => unknown ,
158158 ) : { dispose ( ) : void } ;
159159}
160+ interface CelSearchService {
161+ // `SearchProviderType`: file=0, text=1, aiText=2. Schema is the URI
162+ // scheme the provider answers for - "file" for local workspace content.
163+ registerSearchResultProvider (
164+ scheme : string ,
165+ type : number ,
166+ provider : unknown ,
167+ ) : { dispose ( ) : void } ;
168+ }
160169interface CelServices {
161170 Statusbar : CelStatusbarService ;
162171 Commands : CelCommandService ;
163172 CommandRegistry : CelCommandRegistry ;
173+ Search : CelSearchService ;
164174}
165175
166176function GetServices ( ) : CelServices | null {
@@ -663,6 +673,183 @@ export async function InstallSkyBridge(): Promise<void> {
663673 }
664674 } ) ;
665675
676+ // ---- Search result provider (Land-native) ----
677+ //
678+ // Stock VS Code web's `RemoteSearchService` constructs a
679+ // `LocalFileSearchWorkerClient` which calls
680+ // `HTMLFileSystemProvider.getHandle(folderUri)` to obtain a File System
681+ // Access API directory handle. Land's filesystem goes through Mountain
682+ // over Tauri IPC, not the browser's FSA API, so the handle resolve
683+ // returns `undefined` and the search viewlet silently returns zero
684+ // results. The fix: register a provider that routes to Mountain's
685+ // existing `search:findFiles` / `search:findInFiles` handlers via
686+ // `MountainIPCInvoke`. Registered for the `file` scheme under both
687+ // SearchProviderType.file (0) and SearchProviderType.text (1) so both
688+ // the Search viewlet text queries and file-name filter hit it.
689+ //
690+ // Registration is best-effort - if `__CEL_SERVICES__.Search` isn't
691+ // populated yet (workbench still booting), wait for the
692+ // `cel:workbench-ready` event fired by ExposeWorkbenchAccessor.
693+ const RegisterLandSearchProvider = ( ) => {
694+ const Services = GetServices ( ) ;
695+ if ( ! Services ?. Search ?. registerSearchResultProvider ) return false ;
696+
697+ // Extract the single-folder root URI from a query - Mountain's
698+ // search handlers take the active workspace folder, not a set.
699+ // Multi-root queries fan out over each folder; first one wins for
700+ // now (Land's scanner is single-root in the debug profile).
701+ const FolderFromQuery = ( Query : any ) : string | null => {
702+ const Folder =
703+ Query ?. folderQueries ?. [ 0 ] ?. folder ?? Query ?. folder ?? null ;
704+ if ( ! Folder ) return null ;
705+ if ( typeof Folder === "string" ) return Folder ;
706+ const Path = Folder ?. fsPath ?? Folder ?. path ?? "" ;
707+ return Path || null ;
708+ } ;
709+
710+ // Translate a raw Mountain hit into the `IFileMatch` shape the
711+ // workbench renderer expects. `resource` must carry `$mid:1`
712+ // so VS Code's `URI.revive()` path restores it.
713+ const MatchFromHit = ( Hit : any ) => {
714+ const Raw = String ( Hit ?. uri ?? "" ) ;
715+ const OsPath = Raw . replace ( / ^ f i l e : \/ \/ / , "" ) ;
716+ const Line = Number ( Hit ?. lineNumber ?? 1 ) ;
717+ const Preview = String ( Hit ?. preview ?? "" ) ;
718+ return {
719+ resource : { $mid : 1 , path : OsPath , scheme : "file" } ,
720+ results : [
721+ {
722+ preview : { text : Preview , matches : [ ] } ,
723+ ranges : [
724+ {
725+ startLineNumber : Line ,
726+ startColumn : 1 ,
727+ endLineNumber : Line ,
728+ endColumn : Math . max ( 1 , Preview . length + 1 ) ,
729+ } ,
730+ ] ,
731+ } ,
732+ ] ,
733+ } ;
734+ } ;
735+
736+ const Provider = {
737+ getAIName : async ( ) => undefined ,
738+ textSearch : async (
739+ Query : any ,
740+ OnProgress ?: ( Item : unknown ) => void ,
741+ _Token ?: unknown ,
742+ ) => {
743+ const Pattern = String ( Query ?. contentPattern ?. pattern ?? "" ) ;
744+ if ( ! Pattern ) {
745+ return { results : [ ] , messages : [ ] , limitHit : false } ;
746+ }
747+ const IsRegex = Boolean ( Query ?. contentPattern ?. isRegExp ) ;
748+ const IsCaseSensitive = Boolean (
749+ Query ?. contentPattern ?. isCaseSensitive ,
750+ ) ;
751+ const IsWordMatch = Boolean ( Query ?. contentPattern ?. isWordMatch ) ;
752+ const Include =
753+ Object . keys ( Query ?. includePattern ?? { } ) [ 0 ] ?? "**" ;
754+ const Exclude = Object . keys ( Query ?. excludePattern ?? { } ) [ 0 ] ?? "" ;
755+ const MaxResults = Number ( Query ?. maxResults ?? 1000 ) ;
756+ try {
757+ const Raw = ( await invoke ( "MountainIPCInvoke" , {
758+ method : "search:findInFiles" ,
759+ params : [
760+ Pattern ,
761+ IsRegex ,
762+ IsCaseSensitive ,
763+ IsWordMatch ,
764+ Include ,
765+ Exclude ,
766+ MaxResults ,
767+ ] ,
768+ } ) ) as any [ ] ;
769+ const Results : any [ ] = [ ] ;
770+ for ( const Hit of Raw ?? [ ] ) {
771+ const Match = MatchFromHit ( Hit ) ;
772+ OnProgress ?.( Match ) ;
773+ Results . push ( Match ) ;
774+ }
775+ return {
776+ results : Results ,
777+ messages : [ ] ,
778+ limitHit : Results . length >= MaxResults ,
779+ } ;
780+ } catch ( Error ) {
781+ console . warn ( "[SkyBridge] textSearch failed" , Error ) ;
782+ return { results : [ ] , messages : [ ] , limitHit : false } ;
783+ }
784+ } ,
785+ fileSearch : async ( Query : any , _Token ?: unknown ) => {
786+ // IFileQuery.filePattern is the user's typed filename
787+ // fragment (e.g. "set" matches "settings.ts"). Mountain's
788+ // `search:findFiles` takes a glob, so wrap the fragment
789+ // as `**/<pattern>*` to get prefix-substring matching -
790+ // a close approximation to VS Code's fuzzy file matcher.
791+ const Raw = String ( Query ?. filePattern ?? "" ) . trim ( ) ;
792+ const FolderRoot = FolderFromQuery ( Query ) ;
793+ const Glob = Raw
794+ ? `**/*${ Raw } *`
795+ : Object . keys ( Query ?. includePattern ?? { } ) [ 0 ] ?? "**" ;
796+ const MaxResults = Number ( Query ?. maxResults ?? 500 ) ;
797+ try {
798+ const Files = ( await invoke ( "MountainIPCInvoke" , {
799+ method : "search:findFiles" ,
800+ params : [ Glob , MaxResults ] ,
801+ } ) ) as string [ ] ;
802+ const Results = ( Files ?? [ ] ) . map ( ( Uri ) => ( {
803+ resource : {
804+ $mid : 1 ,
805+ path : String ( Uri ) . replace ( / ^ f i l e : \/ \/ / , "" ) ,
806+ scheme : "file" ,
807+ } ,
808+ } ) ) ;
809+ // Suppress unused warning - FolderRoot would be used
810+ // by a multi-folder fan-out that we don't need yet.
811+ void FolderRoot ;
812+ return {
813+ results : Results ,
814+ messages : [ ] ,
815+ limitHit : Results . length >= MaxResults ,
816+ } ;
817+ } catch ( Error ) {
818+ console . warn ( "[SkyBridge] fileSearch failed" , Error ) ;
819+ return { results : [ ] , messages : [ ] , limitHit : false } ;
820+ }
821+ } ,
822+ clearCache : async ( _Key : string ) => undefined ,
823+ } ;
824+
825+ try {
826+ Services . Search . registerSearchResultProvider ( "file" , 0 , Provider ) ; // file
827+ Services . Search . registerSearchResultProvider ( "file" , 1 , Provider ) ; // text
828+ return true ;
829+ } catch ( Error ) {
830+ console . warn (
831+ "[SkyBridge] registerSearchResultProvider failed" ,
832+ Error ,
833+ ) ;
834+ return false ;
835+ }
836+ } ;
837+
838+ if ( ! RegisterLandSearchProvider ( ) ) {
839+ const OnReady = ( ) => {
840+ window . removeEventListener (
841+ "cel:workbench-ready" ,
842+ OnReady as EventListener ,
843+ ) ;
844+ RegisterLandSearchProvider ( ) ;
845+ } ;
846+ window . addEventListener (
847+ "cel:workbench-ready" ,
848+ OnReady as EventListener ,
849+ { once : true } ,
850+ ) ;
851+ }
852+
666853 // ---- SCM bridge (diagnostic only) ----
667854 // Mountain emits `sky://scm/{register,unregister,updateGroup}` when
668855 // extensions call `vscode.scm.createSourceControl(...)`, but the stock
0 commit comments