Skip to content

Commit 081f3f3

Browse files
author
Andy Hanson
committed
Clean up code for nonrelative path completions
1 parent c4a504b commit 081f3f3

File tree

5 files changed

+50
-63
lines changed

5 files changed

+50
-63
lines changed

src/services/pathCompletions.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ namespace ts.Completions.PathCompletions {
137137
if (directories) {
138138
for (const directory of directories) {
139139
const directoryName = getBaseFileName(normalizePath(directory));
140-
141140
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
142141
}
143142
}
@@ -177,19 +176,33 @@ namespace ts.Completions.PathCompletions {
177176
}
178177
}
179178

180-
if (compilerOptions.moduleResolution === ModuleResolutionKind.NodeJs) {
181-
forEachAncestorDirectory(scriptPath, ancestor => {
182-
const nodeModules = combinePaths(ancestor, "node_modules");
183-
if (host.directoryExists(nodeModules)) {
184-
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
185-
}
186-
});
179+
const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined;
180+
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
181+
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
187182
}
188183

189-
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, result);
190-
191-
for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) {
192-
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
184+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) {
185+
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
186+
// (But do if we didn't find anything, e.g. 'package.json' missing.)
187+
let foundGlobal = false;
188+
if (fragmentDirectory === undefined) {
189+
const oldLength = result.length;
190+
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, result);
191+
for (const moduleName of getNamesFromVisibleNodeModules(fragmentDirectory, scriptPath, host)) {
192+
if (!result.some(entry => entry.name === moduleName)) {
193+
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
194+
}
195+
}
196+
foundGlobal = result.length !== oldLength;
197+
}
198+
if (!foundGlobal) {
199+
forEachAncestorDirectory(scriptPath, ancestor => {
200+
const nodeModules = combinePaths(ancestor, "node_modules");
201+
if (host.directoryExists(nodeModules)) {
202+
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
203+
}
204+
});
205+
}
193206
}
194207

195208
return result;
@@ -228,7 +241,7 @@ namespace ts.Completions.PathCompletions {
228241
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
229242
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
230243

231-
const fragmentHasPath = stringContains(fragment, directorySeparator);
244+
const fragmentHasPath = containsSlash(fragment);
232245

233246
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
234247
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
@@ -262,45 +275,31 @@ namespace ts.Completions.PathCompletions {
262275
return path[0] === directorySeparator ? path.slice(1) : path;
263276
}
264277

265-
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
266-
// Check If this is a nested module
267-
const isNestedModule = stringContains(fragment, directorySeparator);
268-
const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined;
269-
278+
function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): ReadonlyArray<string> {
270279
// Get modules that the type checker picked up
271-
const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name));
272-
let nonRelativeModuleNames = filter(ambientModules, moduleName => startsWith(moduleName, fragment));
280+
const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name));
281+
const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment));
273282

274283
// Nested modules of the form "module-name/sub" need to be adjusted to only return the string
275284
// after the last '/' that appears in the fragment because that's where the replacement span
276285
// starts
277-
if (isNestedModule) {
278-
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment);
279-
nonRelativeModuleNames = map(nonRelativeModuleNames, nonRelativeModuleName => {
280-
return removePrefix(nonRelativeModuleName, moduleNameWithSeperator);
281-
});
286+
if (fragmentDirectory !== undefined) {
287+
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(fragmentDirectory);
288+
return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeperator));
282289
}
290+
return nonRelativeModuleNames;
291+
}
283292

284-
285-
if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) {
286-
for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
287-
if (!isNestedModule) {
288-
nonRelativeModuleNames.push(visibleModule.moduleName);
289-
}
290-
else if (startsWith(visibleModule.moduleName, moduleNameFragment)) {
291-
const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/ undefined, /*include*/ ["./*"]);
292-
if (nestedFiles) {
293-
for (let f of nestedFiles) {
294-
f = normalizePath(f);
295-
const nestedModule = removeFileExtension(getBaseFileName(f));
296-
nonRelativeModuleNames.push(nestedModule);
297-
}
298-
}
299-
}
293+
function getNamesFromVisibleNodeModules(fragmentDirectory: string | undefined, scriptPath: string, host: LanguageServiceHost): string[] {
294+
return flatMap(enumerateNodeModulesVisibleToScript(host, scriptPath), visibleModule => {
295+
if (fragmentDirectory === undefined) {
296+
return visibleModule.moduleName;
300297
}
301-
}
302-
303-
return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive);
298+
else if (startsWith(visibleModule.moduleName, fragmentDirectory)) {
299+
const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/ undefined, /*include*/ ["./*"]);
300+
return map(nestedFiles, f => removeFileExtension(getBaseFileName(normalizePath(f))));
301+
}
302+
});
304303
}
305304

306305
export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): ReadonlyArray<PathCompletion> | undefined {
@@ -522,4 +521,8 @@ namespace ts.Completions.PathCompletions {
522521
catch { /*ignore*/ }
523522
return undefined;
524523
}
524+
525+
function containsSlash(fragment: string) {
526+
return stringContains(fragment, directorySeparator);
527+
}
525528
}

tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,4 @@
2727
// @Filename: ambient.ts
2828
//// declare module "fake-module/other"
2929

30-
const kinds = ["import_as", "import_equals", "require"];
31-
32-
for (const kind of kinds) {
33-
goTo.marker(kind + "0");
34-
verify.completionListContains("repeated");
35-
verify.completionListContains("other");
36-
verify.not.completionListItemsCountIsGreaterThan(2);
37-
}
30+
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["other", "repeated"], { isNewIdentifierLocation: true })

tests/cases/fourslash/completionForStringLiteralNonrelativeImport3.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,4 @@
2727
// @Filename: node_modules/fake-module/repeated.jsx
2828
//// /*repeatedjsx*/
2929

30-
const kinds = ["import_as", "import_equals", "require"];
31-
32-
for (const kind of kinds) {
33-
goTo.marker(kind + "0");
34-
verify.completionListContains("ts");
35-
verify.completionListContains("tsx");
36-
verify.completionListContains("dts");
37-
verify.not.completionListItemsCountIsGreaterThan(3);
38-
}
30+
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["dts", "js", "jsx", "repeated", "ts", "tsx"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionListInImportClause05.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
// NOTE: The node_modules folder is in "/", rather than ".", because it requires
1313
// less scaffolding to mock. In particular, "/" is where we look for type roots.
1414

15-
verify.completionsAt("1", ["@a/b", "@c/d", "@e/f"], { isNewIdentifierLocation: true });
15+
verify.completionsAt("1", ["@e/f", "@a/b", "@c/d"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionsPaths.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,5 @@
2323
// @Filename: /src/folder/4.ts
2424
////const foo = require(`x//*4*/`);
2525

26-
const [r0, r1, r2, r3] = test.ranges();
2726
verify.completionsAt("1", ["y", "x"], { isNewIdentifierLocation: true });
2827
verify.completionsAt(["2", "3", "4"], ["bar", "foo"], { isNewIdentifierLocation: true });

0 commit comments

Comments
 (0)