@@ -448,10 +448,28 @@ namespace ts.Completions.StringCompletions {
448448
449449 fragment = ensureTrailingDirectorySeparator ( fragment ) ;
450450
451- // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
452451 const absolutePath = resolvePath ( scriptPath , fragment ) ;
453452 const baseDirectory = hasTrailingDirectorySeparator ( absolutePath ) ? absolutePath : getDirectoryPath ( absolutePath ) ;
454453
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+
455473 const ignoreCase = ! ( host . useCaseSensitiveFileNames && host . useCaseSensitiveFileNames ( ) ) ;
456474 if ( ! tryDirectoryExists ( host , baseDirectory ) ) return result ;
457475
@@ -505,37 +523,51 @@ namespace ts.Completions.StringCompletions {
505523 }
506524 }
507525
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-
523526 return result ;
524527 }
525528
529+ /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */
526530 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 ;
527533 for ( const path in paths ) {
528534 if ( ! hasProperty ( paths , path ) ) continue ;
529535 const patterns = paths [ path ] ;
530536 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+ } ) ;
536562 }
537563 }
538564 }
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 ;
539571 }
540572
541573 /**
@@ -659,11 +691,15 @@ namespace ts.Completions.StringCompletions {
659691
660692 const pathPrefix = path . slice ( 0 , path . length - 1 ) ;
661693 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 ) ) ;
664700
665701 function justPathMappingName ( name : string ) : readonly NameAndKind [ ] {
666- return startsWith ( name , fragment ) ? [ directoryResult ( name ) ] : emptyArray ;
702+ return startsWith ( name , fragment ) ? [ directoryResult ( removeTrailingDirectorySeparator ( name ) ) ] : emptyArray ;
667703 }
668704 }
669705
0 commit comments