@@ -46,6 +46,7 @@ import {
46
46
import {
47
47
convertStringtoSymbolKind ,
48
48
defInfoToSymbolDescriptor ,
49
+ getMatchScore ,
49
50
isLocalUri ,
50
51
isSymbolDescriptorMatch ,
51
52
normalizeUri ,
@@ -576,33 +577,52 @@ export class TypeScriptService {
576
577
// TODO stream 50 results, then re-query and stream the rest
577
578
const limit = Math . min ( params . limit || Infinity , 50 ) ;
578
579
579
- const query = params . query ;
580
- const symbolQuery = params . symbol && { ...params . symbol } ;
581
-
582
- if ( symbolQuery && symbolQuery . package ) {
583
- // Strip all fields except name from PackageDescriptor
584
- symbolQuery . package = { name : symbolQuery . package . name } ;
585
- }
586
-
587
580
// Return cached result for empty query, if available
588
- if ( ! query && ! symbolQuery && this . emptyQueryWorkspaceSymbols ) {
581
+ if ( ! params . query && ! params . symbol && this . emptyQueryWorkspaceSymbols ) {
589
582
return this . emptyQueryWorkspaceSymbols ;
590
583
}
591
584
592
- // Use special logic for DefinitelyTyped
593
- return this . isDefinitelyTyped
594
- . mergeMap ( isDefinitelyTyped => {
585
+ /** A sorted array that keeps track of symbol match scores to determine the index to insert the symbol at */
586
+ const scores : number [ ] = [ ] ;
587
+
588
+ let observable = this . isDefinitelyTyped
589
+ . mergeMap ( ( isDefinitelyTyped : boolean ) : Observable < [ number , SymbolInformation ] > => {
590
+ // Use special logic for DefinitelyTyped
591
+ // Search only in the correct subdirectory for the given PackageDescriptor
595
592
if ( isDefinitelyTyped ) {
596
- return this . _workspaceSymbolDefinitelyTyped ( { ...params , limit } , span ) ;
597
- }
593
+ // Error if not passed a SymbolDescriptor query with an `@types` PackageDescriptor
594
+ if ( ! params . symbol || ! params . symbol . package || ! params . symbol . package . name || ! params . symbol . package . name . startsWith ( '@types/' ) ) {
595
+ return Observable . throw ( 'workspace/symbol on DefinitelyTyped is only supported with a SymbolDescriptor query with an @types PackageDescriptor' ) ;
596
+ }
598
597
599
- // A workspace/symol request searches all symbols in own code, but not in dependencies
600
- let patches = Observable . from ( this . projectManager . ensureOwnFiles ( span ) )
601
- . mergeMap ( ( ) =>
602
- symbolQuery && symbolQuery . package
598
+ // Fetch all files in the package subdirectory
599
+ // All packages are in the types/ subdirectory
600
+ const normRootUri = this . rootUri . endsWith ( '/' ) ? this . rootUri : this . rootUri + '/' ;
601
+ const packageRootUri = normRootUri + params . symbol . package . name . substr ( 1 ) + '/' ;
602
+
603
+ return Observable . from ( this . updater . ensureStructure ( span ) )
604
+ . mergeMap ( ( ) => Observable . from < string > ( this . inMemoryFileSystem . uris ( ) as any ) )
605
+ . filter ( uri => uri . startsWith ( packageRootUri ) )
606
+ . mergeMap ( uri => this . updater . ensure ( uri , span ) )
607
+ . toArray ( )
608
+ . mergeMap ( ( ) => {
609
+ span . log ( { event : 'fetched package files' } ) ;
610
+ const config = this . projectManager . getParentConfiguration ( packageRootUri , 'ts' ) ;
611
+ if ( ! config ) {
612
+ throw new Error ( `Could not find tsconfig for ${ packageRootUri } ` ) ;
613
+ }
614
+ // Don't match PackageDescriptor on symbols
615
+ return this . _getSymbolsInConfig ( config , params . query || omit ( params . symbol ! , 'package' ) , limit , span ) ;
616
+ } ) ;
617
+ }
618
+ // Regular workspace symbol search
619
+ // Search all symbols in own code, but not in dependencies
620
+ return Observable . from ( this . projectManager . ensureOwnFiles ( span ) )
621
+ . mergeMap < void , ProjectConfiguration > ( ( ) =>
622
+ params . symbol && params . symbol . package && params . symbol . package . name
603
623
// If SymbolDescriptor query with PackageDescriptor, search for package.jsons with matching package name
604
624
? Observable . from < string > ( this . packageManager . packageJsonUris ( ) as any )
605
- . filter ( packageJsonUri => ! symbolQuery || ! symbolQuery . package || ! symbolQuery . package . name || ( JSON . parse ( this . inMemoryFileSystem . getContent ( packageJsonUri ) ) as PackageJson ) . name === symbolQuery . package ! . name )
625
+ . filter ( packageJsonUri => ( JSON . parse ( this . inMemoryFileSystem . getContent ( packageJsonUri ) ) as PackageJson ) . name === params . symbol ! . package ! . name )
606
626
// Find their parent and child tsconfigs
607
627
. mergeMap ( packageJsonUri => Observable . merge (
608
628
castArray < ProjectConfiguration > ( this . projectManager . getParentConfiguration ( packageJsonUri ) || [ ] ) ,
@@ -613,70 +633,32 @@ export class TypeScriptService {
613
633
: this . projectManager . configurations ( ) as any
614
634
)
615
635
// If PackageDescriptor is given, only search project with the matching package name
616
- . mergeMap < ProjectConfiguration , SymbolInformation > ( config => this . _collectWorkspaceSymbols ( config , query || symbolQuery , limit , span ) as any )
617
- . filter ( symbol => ! symbol . location . uri . includes ( '/node_modules/' ) )
618
- // Filter duplicate symbols
619
- // There may be few configurations that contain the same file(s)
620
- // or files from different configurations may refer to the same file(s)
621
- . distinct ( symbol => hashObject ( symbol , { respectType : false } as any ) )
622
- . take ( limit )
623
- . map ( symbol => ( { op : 'add' , path : '/-' , value : symbol } ) as AddPatch )
624
- . startWith ( { op : 'add' , path : '' , value : [ ] } ) ;
625
-
626
- if ( ! query && ! symbolQuery ) {
627
- patches = this . emptyQueryWorkspaceSymbols = patches . publishReplay ( ) . refCount ( ) ;
636
+ . mergeMap ( config => this . _getSymbolsInConfig ( config , params . query || params . symbol , limit , span ) ) ;
637
+ } )
638
+ // Filter symbols found in dependencies
639
+ . filter ( ( [ score , symbol ] ) => ! symbol . location . uri . includes ( '/node_modules/' ) )
640
+ // Filter duplicate symbols
641
+ // There may be few configurations that contain the same file(s)
642
+ // or files from different configurations may refer to the same file(s)
643
+ . distinct ( symbol => hashObject ( symbol , { respectType : false } as any ) )
644
+ . take ( limit )
645
+ // Find out at which index to insert the symbol to maintain sorting order by score
646
+ . map ( ( [ score , symbol ] ) => {
647
+ const index = scores . findIndex ( s => s < score ) ;
648
+ if ( index === - 1 ) {
649
+ scores . push ( score ) ;
650
+ return { op : 'add' , path : '/-' , value : symbol } as AddPatch ;
628
651
}
652
+ scores . splice ( index , 0 , score ) ;
653
+ return { op : 'add' , path : '/' + index , value : symbol } as AddPatch ;
654
+ } )
655
+ . startWith ( { op : 'add' , path : '' , value : [ ] } ) ;
629
656
630
- return patches ;
631
- } ) ;
632
- }
633
-
634
- /**
635
- * Specialised version of workspaceSymbol for DefinitelyTyped.
636
- * Searches only in the correct subdirectory for the given PackageDescriptor.
637
- * Will error if not passed a SymbolDescriptor query with an `@types` PackageDescriptor
638
- *
639
- * @return Observable of JSON Patches that build a `SymbolInformation[]` result
640
- */
641
- protected _workspaceSymbolDefinitelyTyped ( params : WorkspaceSymbolParams , childOf = new Span ( ) ) : Observable < OpPatch > {
642
- const span = childOf . tracer ( ) . startSpan ( 'Handle workspace/symbol DefinitelyTyped' , { childOf } ) ;
643
-
644
- if ( ! params . symbol || ! params . symbol . package || ! params . symbol . package . name || ! params . symbol . package . name . startsWith ( '@types/' ) ) {
645
- return Observable . throw ( 'workspace/symbol on DefinitelyTyped is only supported with a SymbolDescriptor query with an @types PackageDescriptor' ) ;
657
+ if ( ! params . query && ! params . symbol ) {
658
+ observable = this . emptyQueryWorkspaceSymbols = observable . publishReplay ( ) . refCount ( ) ;
646
659
}
647
660
648
- const symbolQuery = { ...params . symbol } ;
649
- // Don't match PackageDescriptor on symbols
650
- symbolQuery . package = undefined ;
651
-
652
- // Fetch all files in the package subdirectory
653
- // All packages are in the types/ subdirectory
654
- const normRootUri = this . rootUri . endsWith ( '/' ) ? this . rootUri : this . rootUri + '/' ;
655
- const packageRootUri = normRootUri + params . symbol . package . name . substr ( 1 ) + '/' ;
656
-
657
- return Observable . from ( this . updater . ensureStructure ( span ) )
658
- . mergeMap ( ( ) => Observable . from < string > ( this . inMemoryFileSystem . uris ( ) as any ) )
659
- . filter ( uri => uri . startsWith ( packageRootUri ) )
660
- . mergeMap ( uri => this . updater . ensure ( uri , span ) )
661
- . toArray ( )
662
- . mergeMap < any , SymbolInformation > ( ( ) => {
663
- span . log ( { event : 'fetched package files' } ) ;
664
-
665
- // Search symbol in configuration
666
- // forcing TypeScript mode
667
- const config = this . projectManager . getConfiguration ( uri2path ( packageRootUri ) , 'ts' ) ;
668
- return this . _collectWorkspaceSymbols ( config , params . query || symbolQuery , params . limit , span ) as any ;
669
- } )
670
- . map ( symbol => ( { op : 'add' , path : '/-' , value : symbol } ) as AddPatch )
671
- . startWith ( { op : 'add' , path : '' , value : [ ] } )
672
- . catch < OpPatch , never > ( err => {
673
- span . setTag ( 'error' , true ) ;
674
- span . log ( { 'event' : 'error' , 'error.object' : err , 'message' : err . message , 'stack' : err . stack } ) ;
675
- throw err ;
676
- } )
677
- . finally ( ( ) => {
678
- span . finish ( ) ;
679
- } ) ;
661
+ return observable ;
680
662
}
681
663
682
664
/**
@@ -1128,8 +1110,8 @@ export class TypeScriptService {
1128
1110
* @param limit An optional limit that is passed to TypeScript
1129
1111
* @return Observable of SymbolInformations
1130
1112
*/
1131
- private _collectWorkspaceSymbols ( config : ProjectConfiguration , query ?: string | Partial < SymbolDescriptor > , limit = Infinity , childOf = new Span ( ) ) : Observable < SymbolInformation > {
1132
- const span = childOf . tracer ( ) . startSpan ( 'Collect workspace symbols' , { childOf } ) ;
1113
+ protected _getSymbolsInConfig ( config : ProjectConfiguration , query ?: string | Partial < SymbolDescriptor > , limit = Infinity , childOf = new Span ( ) ) : Observable < [ number , SymbolInformation ] > {
1114
+ const span = childOf . tracer ( ) . startSpan ( 'Get symbols in config ' , { childOf } ) ;
1133
1115
span . addTags ( { config : config . configFilePath , query, limit } ) ;
1134
1116
1135
1117
return ( ( ) => {
@@ -1142,35 +1124,40 @@ export class TypeScriptService {
1142
1124
}
1143
1125
1144
1126
if ( query ) {
1145
- let items : Observable < ts . NavigateToItem > ;
1127
+ let items : Observable < [ number , ts . NavigateToItem ] > ;
1146
1128
if ( typeof query === 'string' ) {
1147
1129
// Query by text query
1148
- items = Observable . from ( config . getService ( ) . getNavigateToItems ( query , limit , undefined , false ) ) ;
1130
+ items = Observable . from ( config . getService ( ) . getNavigateToItems ( query , limit , undefined , false ) )
1131
+ // Same score for all
1132
+ . map ( item => [ 1 , item ] ) ;
1149
1133
} else {
1150
1134
const queryWithoutPackage = omit ( query , 'package' ) as SymbolDescriptor ;
1151
1135
// Query by name
1152
1136
items = Observable . from ( config . getService ( ) . getNavigateToItems ( query . name || '' , limit , undefined , false ) )
1153
- // First filter to match SymbolDescriptor, ignoring PackageDescriptor
1154
- . filter ( item => isSymbolDescriptorMatch ( queryWithoutPackage , {
1137
+ // Get a score how good the symbol matches the SymbolDescriptor ( ignoring PackageDescriptor)
1138
+ . map ( ( item ) : [ number , ts . NavigateToItem ] => [ getMatchScore ( queryWithoutPackage , {
1155
1139
kind : item . kind ,
1156
1140
name : item . name ,
1157
1141
containerKind : item . containerKind ,
1158
1142
containerName : item . containerName
1159
- } ) )
1160
- // if SymbolDescriptor matched, get package.json and match PackageDescriptor name
1143
+ } ) , item ] )
1144
+ // If score === 0, no properties matched
1145
+ . filter ( ( [ score , symbol ] ) => score > 0 )
1146
+ // If SymbolDescriptor matched, get package.json and match PackageDescriptor name
1161
1147
// TODO get and match full PackageDescriptor (version)
1162
- . mergeMap ( item => {
1148
+ . mergeMap ( ( [ score , item ] ) => {
1163
1149
if ( ! query . package || ! query . package . name ) {
1164
- return [ item ] ;
1150
+ return [ [ score , item ] ] ;
1165
1151
}
1166
1152
const uri = path2uri ( '' , item . fileName ) ;
1167
1153
return Observable . from ( this . packageManager . getClosestPackageJson ( uri , span ) )
1168
- . mergeMap ( packageJson => packageJson && packageJson . name === query . package ! . name ! ? [ item ] : [ ] ) ;
1154
+ // If PackageDescriptor matches, increase score
1155
+ . map ( ( packageJson ) : [ number , ts . NavigateToItem ] => packageJson && packageJson . name === query . package ! . name ! ? [ score + 1 , item ] : [ score , item ] ) ;
1169
1156
} ) ;
1170
1157
}
1171
1158
return Observable . from ( items )
1172
1159
// Map NavigateToItems to SymbolInformations
1173
- . map ( item => {
1160
+ . map ( ( [ score , item ] ) => {
1174
1161
const sourceFile = program . getSourceFile ( item . fileName ) ;
1175
1162
if ( ! sourceFile ) {
1176
1163
throw new Error ( `Source file ${ item . fileName } does not exist` ) ;
@@ -1189,13 +1176,16 @@ export class TypeScriptService {
1189
1176
if ( item . containerName ) {
1190
1177
symbolInformation . containerName = item . containerName ;
1191
1178
}
1192
- return symbolInformation ;
1179
+ return [ score , symbolInformation ] as [ number , SymbolInformation ] ;
1193
1180
} )
1194
- . filter ( symbolInformation => isLocalUri ( symbolInformation . location . uri ) ) ;
1181
+ . filter ( ( [ score , symbolInformation ] ) => isLocalUri ( symbolInformation . location . uri ) ) ;
1195
1182
} else {
1196
1183
// An empty query uses a different algorithm to iterate all files and aggregate the symbols per-file to get all symbols
1197
1184
// TODO make all implementations use this? It has the advantage of being streamable and cancellable
1198
- return Observable . from < SymbolInformation > ( this . _getNavigationTreeItems ( config ) as any ) . take ( limit ) ;
1185
+ return Observable . from < SymbolInformation > ( this . _getNavigationTreeItems ( config ) as any )
1186
+ // Same score for all
1187
+ . map ( symbol => [ 1 , symbol ] )
1188
+ . take ( limit ) ;
1199
1189
}
1200
1190
} catch ( err ) {
1201
1191
return Observable . throw ( err ) ;
0 commit comments