diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index b0dbfff0216ce..7d5fd296cc6e6 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -1,8 +1,10 @@ /* @internal */ namespace ts.Completions.PathCompletions { - export interface PathCompletion { + export interface NameAndKind { readonly name: string; readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName; + } + export interface PathCompletion extends NameAndKind { readonly span: TextSpan; } function createPathCompletion(name: string, kind: PathCompletion["kind"], span: TextSpan): PathCompletion { @@ -158,10 +160,10 @@ namespace ts.Completions.PathCompletions { for (const path in paths) { const patterns = paths[path]; if (paths.hasOwnProperty(path) && patterns) { - for (const pathCompletion of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) { + for (const { name, kind } of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) { // Path mappings may provide a duplicate way to get to something we've already added, so don't add again. - if (!result.some(entry => entry.name === pathCompletion)) { - result.push(createPathCompletion(pathCompletion, ScriptElementKind.externalModuleName, span)); + if (!result.some(entry => entry.name === name)) { + result.push(createPathCompletion(name, kind, span)); } } } @@ -188,22 +190,22 @@ namespace ts.Completions.PathCompletions { function getCompletionsForPathMapping( path: string, patterns: ReadonlyArray, fragment: string, baseUrl: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost, - ): ReadonlyArray { + ): ReadonlyArray { if (!endsWith(path, "*")) { // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. - return !stringContains(path, "*") && startsWith(path, fragment) ? [path] : emptyArray; + return !stringContains(path, "*") && startsWith(path, fragment) ? [{ name: path, kind: ScriptElementKind.directory }] : emptyArray; } const pathPrefix = path.slice(0, path.length - 1); if (!startsWith(fragment, pathPrefix)) { - return [pathPrefix]; + return [{ name: pathPrefix, kind: ScriptElementKind.directory }]; } const remainingFragment = fragment.slice(pathPrefix.length); return flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); } - function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost): string[] | undefined { + function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost): ReadonlyArray | undefined { if (!host.readDirectory) { return undefined; } @@ -234,14 +236,14 @@ namespace ts.Completions.PathCompletions { // doesn't support. For now, this is safer but slower const includeGlob = normalizedSuffix ? "**/*" : "./*"; - const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]); - const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)); + const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]).map(name => ({ name, kind: ScriptElementKind.scriptElement })); + const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)).map(name => ({ name, kind: ScriptElementKind.directory })); // Trim away prefix and suffix - return mapDefined(concatenate(matches, directories), match => { - const normalizedMatch = normalizePath(match); + return mapDefined(concatenate(matches, directories), ({ name, kind }) => { + const normalizedMatch = normalizePath(name); const inner = withoutStartAndEnd(normalizedMatch, completePrefix, normalizedSuffix); - return inner !== undefined ? removeLeadingDirectorySeparator(removeFileExtension(inner)) : undefined; + return inner !== undefined ? { name: removeLeadingDirectorySeparator(removeFileExtension(inner)), kind } : undefined; }); } @@ -488,8 +490,8 @@ namespace ts.Completions.PathCompletions { return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; } - function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): string[] | undefined | undefined { - return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include); + function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): ReadonlyArray { + return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray; } function tryReadFile(host: LanguageServiceHost, path: string): string | undefined { diff --git a/tests/cases/fourslash/completionsPaths_kinds.ts b/tests/cases/fourslash/completionsPaths_kinds.ts new file mode 100644 index 0000000000000..152c7d1bf5a5d --- /dev/null +++ b/tests/cases/fourslash/completionsPaths_kinds.ts @@ -0,0 +1,30 @@ +/// + +// @Filename: /src/b.ts +////not read + +// @Filename: /src/dir/x.ts +////not read + +// @Filename: /src/a.ts +////import {} from "./[|/*0*/|]"; +////import {} from "./[|/*1*/|]"; + +// @Filename: /tsconfig.json +////{ +//// "compilerOptions": { +//// "baseUrl": ".", +//// "paths": { +//// "foo/*": ["src/*"] +//// } +//// } +////} + +goTo.marker("0"); +verify.completionListContains("dir", undefined, undefined, "directory"); +verify.completionListContains("b", undefined, undefined, "script"); + +goTo.marker("1"); +verify.completionListContains("dir", undefined, undefined, "directory"); +verify.completionListContains("b", undefined, undefined, "script"); +