Skip to content

Commit 97c3e9c

Browse files
author
Andy
authored
Support all path mappings that end in "*" in completions (microsoft#21072)
* Support all path mappings that end in "*" in completions * Check for uppercase TsConfig.JSON
1 parent 84e3681 commit 97c3e9c

4 files changed

Lines changed: 86 additions & 64 deletions

File tree

src/harness/fourslash.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ namespace FourSlash {
265265
ts.forEach(testData.files, file => {
266266
// Create map between fileName and its content for easily looking up when resolveReference flag is specified
267267
this.inputFiles.set(file.fileName, file.content);
268-
if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") {
268+
if (isTsconfig(file)) {
269269
const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
270270
if (configJson.config === undefined) {
271271
throw new Error(`Failed to parse test tsconfig.json: ${configJson.error.messageText}`);
@@ -3384,7 +3384,7 @@ ${code}
33843384
}
33853385

33863386
// @Filename is the only directive that can be used in a test that contains tsconfig.json file.
3387-
if (containTSConfigJson(files)) {
3387+
if (files.some(isTsconfig)) {
33883388
let directive = getNonFileNameOptionInFileList(files);
33893389
if (!directive) {
33903390
directive = getNonFileNameOptionInObject(globalOptions);
@@ -3403,8 +3403,8 @@ ${code}
34033403
};
34043404
}
34053405

3406-
function containTSConfigJson(files: FourSlashFile[]): boolean {
3407-
return ts.forEach(files, f => f.fileOptions.Filename === "tsconfig.json");
3406+
function isTsconfig(file: FourSlashFile): boolean {
3407+
return ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json";
34083408
}
34093409

34103410
function getNonFileNameOptionInFileList(files: FourSlashFile[]): string {

src/services/completions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ namespace ts.Completions {
277277
// import x = require("/*completion position*/");
278278
// var y = require("/*completion position*/");
279279
// export * from "/*completion position*/";
280-
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(node, compilerOptions, host, typeChecker);
280+
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker);
281281
return pathCompletionsInfo(entries);
282282
}
283283
else if (isIndexedAccessTypeNode(node.parent.parent)) {

src/services/pathCompletions.ts

Lines changed: 62 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* @internal */
22
namespace ts.Completions.PathCompletions {
3-
export function getStringLiteralCompletionsFromModuleNames(node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
3+
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
44
const literalValue = normalizeSlashes(node.text);
55

66
const scriptPath = node.getSourceFile().path;
77
const scriptDirectory = getDirectoryPath(scriptPath);
88

9-
const span = getDirectoryFragmentTextSpan((<StringLiteral>node).text, node.getStart() + 1);
9+
const span = getDirectoryFragmentTextSpan((<StringLiteral>node).text, node.getStart(sourceFile) + 1);
1010
if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) {
1111
const extensions = getSupportedExtensions(compilerOptions);
1212
if (compilerOptions.rootDirs) {
@@ -147,25 +147,15 @@ namespace ts.Completions.PathCompletions {
147147
getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/ false, span, host, /*exclude*/ undefined, result);
148148

149149
for (const path in paths) {
150-
if (!paths.hasOwnProperty(path)) continue;
151150
const patterns = paths[path];
152-
if (!patterns) continue;
153-
154-
if (path === "*") {
155-
for (const pattern of patterns) {
156-
for (const match of getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions, host)) {
157-
// Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
158-
if (result.some(entry => entry.name === match)) continue;
159-
result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName, span));
151+
if (paths.hasOwnProperty(path) && patterns) {
152+
for (const pathCompletion of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) {
153+
// Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
154+
if (!result.some(entry => entry.name === pathCompletion)) {
155+
result.push(createCompletionEntryForModule(pathCompletion, ScriptElementKind.externalModuleName, span));
160156
}
161157
}
162158
}
163-
else if (startsWith(path, fragment)) {
164-
if (patterns.length === 1) {
165-
if (result.some(entry => entry.name === path)) continue;
166-
result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName, span));
167-
}
168-
}
169159
}
170160
}
171161

@@ -187,52 +177,65 @@ namespace ts.Completions.PathCompletions {
187177
return result;
188178
}
189179

190-
function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray<string>, host: LanguageServiceHost): string[] {
191-
if (host.readDirectory) {
192-
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
193-
if (parsed) {
194-
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
195-
// full directory component. For example: directory/path/of/prefix/base*
196-
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
197-
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
198-
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
199-
200-
const fragmentHasPath = stringContains(fragment, directorySeparator);
201-
202-
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
203-
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
204-
205-
const normalizedSuffix = normalizePath(parsed.suffix);
206-
const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory);
207-
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
208-
209-
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
210-
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
211-
// doesn't support. For now, this is safer but slower
212-
const includeGlob = normalizedSuffix ? "**/*" : "./*";
213-
214-
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]);
215-
if (matches) {
216-
const result: string[] = [];
217-
218-
// Trim away prefix and suffix
219-
for (const match of matches) {
220-
const normalizedMatch = normalizePath(match);
221-
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
222-
continue;
223-
}
180+
function getCompletionsForPathMapping(
181+
path: string, patterns: ReadonlyArray<string>, fragment: string, baseUrl: string, fileExtensions: ReadonlyArray<string>, host: LanguageServiceHost,
182+
): string[] {
183+
if (!endsWith(path, "*")) {
184+
// For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion.
185+
return !stringContains(path, "*") && startsWith(path, fragment) ? [path] : emptyArray;
186+
}
224187

225-
const start = completePrefix.length;
226-
const length = normalizedMatch.length - start - normalizedSuffix.length;
188+
const pathPrefix = path.slice(0, path.length - 1);
189+
if (!startsWith(fragment, pathPrefix)) {
190+
return emptyArray;
191+
}
227192

228-
result.push(removeFileExtension(normalizedMatch.substr(start, length)));
229-
}
230-
return result;
231-
}
232-
}
193+
const remainingFragment = fragment.slice(pathPrefix.length);
194+
return flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host));
195+
}
196+
197+
function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray<string>, host: LanguageServiceHost): string[] | undefined {
198+
if (!host.readDirectory) {
199+
return undefined;
233200
}
234201

235-
return undefined;
202+
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
203+
if (!parsed) {
204+
return undefined;
205+
}
206+
207+
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
208+
// full directory component. For example: directory/path/of/prefix/base*
209+
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
210+
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
211+
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
212+
213+
const fragmentHasPath = stringContains(fragment, directorySeparator);
214+
215+
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
216+
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
217+
218+
const normalizedSuffix = normalizePath(parsed.suffix);
219+
const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory);
220+
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
221+
222+
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
223+
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
224+
// doesn't support. For now, this is safer but slower
225+
const includeGlob = normalizedSuffix ? "**/*" : "./*";
226+
227+
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]);
228+
// Trim away prefix and suffix
229+
return mapDefined(matches, match => {
230+
const normalizedMatch = normalizePath(match);
231+
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
232+
return;
233+
}
234+
235+
const start = completePrefix.length;
236+
const length = normalizedMatch.length - start - normalizedSuffix.length;
237+
return removeFileExtension(normalizedMatch.substr(start, length));
238+
});
236239
}
237240

238241
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /src/b.ts
4+
////export const x = 0;
5+
6+
// @Filename: /src/a.ts
7+
////import {} from "foo//**/";
8+
9+
// @Filename: /tsconfig.json
10+
////{
11+
//// "compilerOptions": {
12+
//// "baseUrl": ".",
13+
//// "paths": {
14+
//// "foo/*": ["src/*"]
15+
//// }
16+
//// }
17+
////}
18+
19+
verify.completionsAt("", ["a", "b"]);

0 commit comments

Comments
 (0)