@@ -448,10 +448,28 @@ namespace ts.Completions.StringCompletions {
448
448
449
449
fragment = ensureTrailingDirectorySeparator ( fragment ) ;
450
450
451
- // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
452
451
const absolutePath = resolvePath ( scriptPath , fragment ) ;
453
452
const baseDirectory = hasTrailingDirectorySeparator ( absolutePath ) ? absolutePath : getDirectoryPath ( absolutePath ) ;
454
453
454
+ // check for a version redirect
455
+ const packageJsonPath = findPackageJson ( baseDirectory , host ) ;
456
+ if ( packageJsonPath ) {
457
+ const packageJson = readJson ( packageJsonPath , host as { readFile : ( filename : string ) => string | undefined } ) ;
458
+ const typesVersions = ( packageJson as any ) . typesVersions ;
459
+ if ( typeof typesVersions === "object" ) {
460
+ const versionPaths = getPackageJsonTypesVersionsPaths ( typesVersions ) ?. paths ;
461
+ if ( versionPaths ) {
462
+ const packageDirectory = getDirectoryPath ( packageJsonPath ) ;
463
+ const pathInPackage = absolutePath . slice ( ensureTrailingDirectorySeparator ( packageDirectory ) . length ) ;
464
+ if ( addCompletionEntriesFromPaths ( result , pathInPackage , packageDirectory , extensions , versionPaths , host ) ) {
465
+ // A true result means one of the `versionPaths` was matched, which will block relative resolution
466
+ // to files and folders from here. All reachable paths given the pattern match are already added.
467
+ return result ;
468
+ }
469
+ }
470
+ }
471
+ }
472
+
455
473
const ignoreCase = ! ( host . useCaseSensitiveFileNames && host . useCaseSensitiveFileNames ( ) ) ;
456
474
if ( ! tryDirectoryExists ( host , baseDirectory ) ) return result ;
457
475
@@ -505,37 +523,51 @@ namespace ts.Completions.StringCompletions {
505
523
}
506
524
}
507
525
508
- // check for a version redirect
509
- const packageJsonPath = findPackageJson ( baseDirectory , host ) ;
510
- if ( packageJsonPath ) {
511
- const packageJson = readJson ( packageJsonPath , host as { readFile : ( filename : string ) => string | undefined } ) ;
512
- const typesVersions = ( packageJson as any ) . typesVersions ;
513
- if ( typeof typesVersions === "object" ) {
514
- const versionResult = getPackageJsonTypesVersionsPaths ( typesVersions ) ;
515
- const versionPaths = versionResult && versionResult . paths ;
516
- const rest = absolutePath . slice ( ensureTrailingDirectorySeparator ( baseDirectory ) . length ) ;
517
- if ( versionPaths ) {
518
- addCompletionEntriesFromPaths ( result , rest , baseDirectory , extensions , versionPaths , host ) ;
519
- }
520
- }
521
- }
522
-
523
526
return result ;
524
527
}
525
528
529
+ /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */
526
530
function addCompletionEntriesFromPaths ( result : NameAndKind [ ] , fragment : string , baseDirectory : string , fileExtensions : readonly string [ ] , paths : MapLike < string [ ] > , host : LanguageServiceHost ) {
531
+ let pathResults : { results : NameAndKind [ ] , matchedPattern : boolean } [ ] = [ ] ;
532
+ let matchedPathPrefixLength = - 1 ;
527
533
for ( const path in paths ) {
528
534
if ( ! hasProperty ( paths , path ) ) continue ;
529
535
const patterns = paths [ path ] ;
530
536
if ( patterns ) {
531
- for ( const { name, kind, extension } of getCompletionsForPathMapping ( path , patterns , fragment , baseDirectory , fileExtensions , host ) ) {
532
- // Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
533
- if ( ! result . some ( entry => entry . name === name ) ) {
534
- result . push ( nameAndKind ( name , kind , extension ) ) ;
535
- }
537
+ const pathPattern = tryParsePattern ( path ) ;
538
+ if ( ! pathPattern ) continue ;
539
+ const isMatch = typeof pathPattern === "object" && isPatternMatch ( pathPattern , fragment ) ;
540
+ const isLongestMatch = isMatch && ( matchedPathPrefixLength === undefined || pathPattern . prefix . length > matchedPathPrefixLength ) ;
541
+ if ( isLongestMatch ) {
542
+ // If this is a higher priority match than anything we've seen so far, previous results from matches are invalid, e.g.
543
+ // for `import {} from "some-package/|"` with a typesVersions:
544
+ // {
545
+ // "bar/*": ["bar/*"], // <-- 1. We add 'bar', but 'bar/*' doesn't match yet.
546
+ // "*": ["dist/*"], // <-- 2. We match here and add files from dist. 'bar' is still ok because it didn't come from a match.
547
+ // "foo/*": ["foo/*"] // <-- 3. We matched '*' earlier and added results from dist, but if 'foo/*' also matched,
548
+ // } results in dist would not be visible. 'bar' still stands because it didn't come from a match.
549
+ // This is especially important if `dist/foo` is a folder, because if we fail to clear results
550
+ // added by the '*' match, after typing `"some-package/foo/|"` we would get file results from both
551
+ // ./dist/foo and ./foo, when only the latter will actually be resolvable.
552
+ // See pathCompletionsTypesVersionsWildcard6.ts.
553
+ matchedPathPrefixLength = pathPattern . prefix . length ;
554
+ pathResults = pathResults . filter ( r => ! r . matchedPattern ) ;
555
+ }
556
+ if ( typeof pathPattern === "string" || matchedPathPrefixLength === undefined || pathPattern . prefix . length >= matchedPathPrefixLength ) {
557
+ pathResults . push ( {
558
+ matchedPattern : isMatch ,
559
+ results : getCompletionsForPathMapping ( path , patterns , fragment , baseDirectory , fileExtensions , host )
560
+ . map ( ( { name, kind, extension } ) => nameAndKind ( name , kind , extension ) ) ,
561
+ } ) ;
536
562
}
537
563
}
538
564
}
565
+
566
+ const equatePaths = host . useCaseSensitiveFileNames ?.( ) ? equateStringsCaseSensitive : equateStringsCaseInsensitive ;
567
+ const equateResults : EqualityComparer < NameAndKind > = ( a , b ) => equatePaths ( a . name , b . name ) ;
568
+ pathResults . forEach ( pathResult => pathResult . results . forEach ( pathResult => pushIfUnique ( result , pathResult , equateResults ) ) ) ;
569
+
570
+ return matchedPathPrefixLength > - 1 ;
539
571
}
540
572
541
573
/**
@@ -659,11 +691,15 @@ namespace ts.Completions.StringCompletions {
659
691
660
692
const pathPrefix = path . slice ( 0 , path . length - 1 ) ;
661
693
const remainingFragment = tryRemovePrefix ( fragment , pathPrefix ) ;
662
- return remainingFragment === undefined ? justPathMappingName ( pathPrefix ) : flatMap ( patterns , pattern =>
663
- getModulesForPathsPattern ( remainingFragment , baseUrl , pattern , fileExtensions , host ) ) ;
694
+ if ( remainingFragment === undefined ) {
695
+ const starIsFullPathComponent = path [ path . length - 2 ] === "/" ;
696
+ return starIsFullPathComponent ? justPathMappingName ( pathPrefix ) : flatMap ( patterns , pattern =>
697
+ getModulesForPathsPattern ( "" , baseUrl , pattern , fileExtensions , host ) ?. map ( ( { name, ...rest } ) => ( { name : pathPrefix + name , ...rest } ) ) ) ;
698
+ }
699
+ return flatMap ( patterns , pattern => getModulesForPathsPattern ( remainingFragment , baseUrl , pattern , fileExtensions , host ) ) ;
664
700
665
701
function justPathMappingName ( name : string ) : readonly NameAndKind [ ] {
666
- return startsWith ( name , fragment ) ? [ directoryResult ( name ) ] : emptyArray ;
702
+ return startsWith ( name , fragment ) ? [ directoryResult ( removeTrailingDirectorySeparator ( name ) ) ] : emptyArray ;
667
703
}
668
704
}
669
705
0 commit comments