Support all path mappings that end in "*" in completions (#21072)
* Support all path mappings that end in "*" in completions * Check for uppercase TsConfig.JSON
This commit is contained in:
parent
84e3681b79
commit
97c3e9c3ef
|
@ -265,7 +265,7 @@ namespace FourSlash {
|
|||
ts.forEach(testData.files, file => {
|
||||
// Create map between fileName and its content for easily looking up when resolveReference flag is specified
|
||||
this.inputFiles.set(file.fileName, file.content);
|
||||
if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") {
|
||||
if (isTsconfig(file)) {
|
||||
const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
|
||||
if (configJson.config === undefined) {
|
||||
throw new Error(`Failed to parse test tsconfig.json: ${configJson.error.messageText}`);
|
||||
|
@ -3384,7 +3384,7 @@ ${code}
|
|||
}
|
||||
|
||||
// @Filename is the only directive that can be used in a test that contains tsconfig.json file.
|
||||
if (containTSConfigJson(files)) {
|
||||
if (files.some(isTsconfig)) {
|
||||
let directive = getNonFileNameOptionInFileList(files);
|
||||
if (!directive) {
|
||||
directive = getNonFileNameOptionInObject(globalOptions);
|
||||
|
@ -3403,8 +3403,8 @@ ${code}
|
|||
};
|
||||
}
|
||||
|
||||
function containTSConfigJson(files: FourSlashFile[]): boolean {
|
||||
return ts.forEach(files, f => f.fileOptions.Filename === "tsconfig.json");
|
||||
function isTsconfig(file: FourSlashFile): boolean {
|
||||
return ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json";
|
||||
}
|
||||
|
||||
function getNonFileNameOptionInFileList(files: FourSlashFile[]): string {
|
||||
|
|
|
@ -277,7 +277,7 @@ namespace ts.Completions {
|
|||
// import x = require("/*completion position*/");
|
||||
// var y = require("/*completion position*/");
|
||||
// export * from "/*completion position*/";
|
||||
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(node, compilerOptions, host, typeChecker);
|
||||
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker);
|
||||
return pathCompletionsInfo(entries);
|
||||
}
|
||||
else if (isIndexedAccessTypeNode(node.parent.parent)) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* @internal */
|
||||
namespace ts.Completions.PathCompletions {
|
||||
export function getStringLiteralCompletionsFromModuleNames(node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
|
||||
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
|
||||
const literalValue = normalizeSlashes(node.text);
|
||||
|
||||
const scriptPath = node.getSourceFile().path;
|
||||
const scriptDirectory = getDirectoryPath(scriptPath);
|
||||
|
||||
const span = getDirectoryFragmentTextSpan((<StringLiteral>node).text, node.getStart() + 1);
|
||||
const span = getDirectoryFragmentTextSpan((<StringLiteral>node).text, node.getStart(sourceFile) + 1);
|
||||
if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) {
|
||||
const extensions = getSupportedExtensions(compilerOptions);
|
||||
if (compilerOptions.rootDirs) {
|
||||
|
@ -147,25 +147,15 @@ namespace ts.Completions.PathCompletions {
|
|||
getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/ false, span, host, /*exclude*/ undefined, result);
|
||||
|
||||
for (const path in paths) {
|
||||
if (!paths.hasOwnProperty(path)) continue;
|
||||
const patterns = paths[path];
|
||||
if (!patterns) continue;
|
||||
|
||||
if (path === "*") {
|
||||
for (const pattern of patterns) {
|
||||
for (const match of getModulesForPathsPattern(fragment, baseUrl, pattern, 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 === match)) continue;
|
||||
result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName, span));
|
||||
if (paths.hasOwnProperty(path) && patterns) {
|
||||
for (const pathCompletion 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(createCompletionEntryForModule(pathCompletion, ScriptElementKind.externalModuleName, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (startsWith(path, fragment)) {
|
||||
if (patterns.length === 1) {
|
||||
if (result.some(entry => entry.name === path)) continue;
|
||||
result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,52 +177,65 @@ namespace ts.Completions.PathCompletions {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray<string>, host: LanguageServiceHost): string[] {
|
||||
if (host.readDirectory) {
|
||||
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
|
||||
if (parsed) {
|
||||
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
|
||||
// full directory component. For example: directory/path/of/prefix/base*
|
||||
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
|
||||
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
|
||||
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
|
||||
|
||||
const fragmentHasPath = stringContains(fragment, directorySeparator);
|
||||
|
||||
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
|
||||
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
|
||||
|
||||
const normalizedSuffix = normalizePath(parsed.suffix);
|
||||
const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory);
|
||||
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
|
||||
|
||||
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
|
||||
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
|
||||
// doesn't support. For now, this is safer but slower
|
||||
const includeGlob = normalizedSuffix ? "**/*" : "./*";
|
||||
|
||||
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]);
|
||||
if (matches) {
|
||||
const result: string[] = [];
|
||||
|
||||
// Trim away prefix and suffix
|
||||
for (const match of matches) {
|
||||
const normalizedMatch = normalizePath(match);
|
||||
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const start = completePrefix.length;
|
||||
const length = normalizedMatch.length - start - normalizedSuffix.length;
|
||||
|
||||
result.push(removeFileExtension(normalizedMatch.substr(start, length)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
function getCompletionsForPathMapping(
|
||||
path: string, patterns: ReadonlyArray<string>, fragment: string, baseUrl: string, fileExtensions: ReadonlyArray<string>, host: LanguageServiceHost,
|
||||
): string[] {
|
||||
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 undefined;
|
||||
const pathPrefix = path.slice(0, path.length - 1);
|
||||
if (!startsWith(fragment, pathPrefix)) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
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<string>, host: LanguageServiceHost): string[] | undefined {
|
||||
if (!host.readDirectory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
|
||||
// full directory component. For example: directory/path/of/prefix/base*
|
||||
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
|
||||
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
|
||||
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
|
||||
|
||||
const fragmentHasPath = stringContains(fragment, directorySeparator);
|
||||
|
||||
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
|
||||
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
|
||||
|
||||
const normalizedSuffix = normalizePath(parsed.suffix);
|
||||
const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory);
|
||||
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
|
||||
|
||||
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
|
||||
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
|
||||
// doesn't support. For now, this is safer but slower
|
||||
const includeGlob = normalizedSuffix ? "**/*" : "./*";
|
||||
|
||||
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]);
|
||||
// Trim away prefix and suffix
|
||||
return mapDefined(matches, match => {
|
||||
const normalizedMatch = normalizePath(match);
|
||||
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = completePrefix.length;
|
||||
const length = normalizedMatch.length - start - normalizedSuffix.length;
|
||||
return removeFileExtension(normalizedMatch.substr(start, length));
|
||||
});
|
||||
}
|
||||
|
||||
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
|
||||
|
|
19
tests/cases/fourslash/completionsPaths_pathMapping.ts
Normal file
19
tests/cases/fourslash/completionsPaths_pathMapping.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @Filename: /src/b.ts
|
||||
////export const x = 0;
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
////import {} from "foo//**/";
|
||||
|
||||
// @Filename: /tsconfig.json
|
||||
////{
|
||||
//// "compilerOptions": {
|
||||
//// "baseUrl": ".",
|
||||
//// "paths": {
|
||||
//// "foo/*": ["src/*"]
|
||||
//// }
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.completionsAt("", ["a", "b"]);
|
Loading…
Reference in a new issue