diff --git a/src/services/services.ts b/src/services/services.ts index 71248b007b6bb..108b40cbe5dd8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1374,6 +1374,8 @@ module ts { static constElement = "const"; static letElement = "let"; + + static stringElement = "string"; } export class ScriptElementKindModifier { @@ -2483,6 +2485,11 @@ module ts { log("getCompletionsAtPosition: Get previous token 2: " + (new Date().getTime() - start)); } + // Check of this is an external module name completion location + if (isExternalModuleName(previousToken)) { + return getExternalModulecompletions(previousToken); + } + // Check if this is a valid completion location if (previousToken && isCompletionListBlocker(previousToken)) { log("Returning an empty list because completion was requested in an invalid position."); @@ -2912,6 +2919,63 @@ module ts { return filteredMembers; } + + function isExternalModuleName(token: Node): boolean { + if (token && token.parent && token.kind === SyntaxKind.StringLiteral && isInStringOrRegularExpressionOrTemplateLiteral(token)) { + return token.parent.kind === SyntaxKind.ImportDeclaration || + token.parent.kind === SyntaxKind.ExportDeclaration || + token.parent.kind === SyntaxKind.ExternalModuleReference; + } + return false; + } + + function getExternalModulecompletions(location: Node): CompletionInfo { + var entries: CompletionEntry[] = []; + var currentSourceFile = location.getSourceFile(); + var currentDirectoryPath = getDirectoryPath(normalizePath(currentSourceFile.fileName)); + var isInAmbientContext = ts.isInAmbientContext(location); + + forEach(program.getSourceFiles(), sourceFile => { + if (isExternalModule(sourceFile)) { + // Only consider these if current request is not in an ambient context, + // and in a diffrent file + if (!isInAmbientContext && sourceFile !== currentSourceFile) { + var relativeModuleName = removeFileExtension(getRelativePathToDirectoryOrUrl(currentDirectoryPath, + sourceFile.fileName, "", getCanonicalFileName, /*isAbsolutePathAnUrl*/ true)); + if (relativeModuleName.charCodeAt(0) !== CharacterCodes.dot && relativeModuleName.charCodeAt(1) !== CharacterCodes.slash) { + // always make files relative + relativeModuleName = "./" + relativeModuleName; + } + entries.push({ + name: relativeModuleName, + kind: ScriptElementKind.stringElement, + kindModifiers: ScriptElementKindModifier.none, + }); + } + } + else { + // Look for ambient external module declarations + forEachChild(sourceFile, child => { + if (child.kind === SyntaxKind.ModuleDeclaration && (child).name.kind === SyntaxKind.StringLiteral) { + if (sourceFile !== currentSourceFile || location.pos > child.end || location.end < child.pos) { + entries.push({ + kind: ScriptElementKind.stringElement, + kindModifiers: ScriptElementKindModifier.ambientModifier, + name: (child).name.text + }); + } + } + }); + } + }); + + return { + isMemberCompletion: true, + isNewIdentifierLocation: true, + isBuilder: true, + entries + }; + } } function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { diff --git a/tests/cases/fourslash/externalModuleNameCompletions.ts b/tests/cases/fourslash/externalModuleNameCompletions.ts new file mode 100644 index 0000000000000..f1a16575040d9 --- /dev/null +++ b/tests/cases/fourslash/externalModuleNameCompletions.ts @@ -0,0 +1,29 @@ +/// + +// @Filename: ambients.ts +////declare module "a" {} +////declare module "b" {} + +// @Filename: moduleC.ts +////export var x = 0; + +// @Filename: global.ts +////var global = 0; + +// @Filename: current.ts +////import * from "/*1*/"; +////import d from "/*2*/"; +////import {a as A} from "/*3*/"; +////import * as NS from "/*4*/"; +////import "/*5*/"; +////import x = require("/*6*/"); +////export * from "/*7*/"; +////export {a as A} from "/*8*/"; + +test.markers().forEach(m => { + goTo.position(m.position, m.fileName); + verify.memberListContains("a"); + verify.memberListContains("b"); + verify.memberListContains("./moduleC"); + verify.memberListCount(3); +}); diff --git a/tests/cases/fourslash/externalModuleNameCompletions2.ts b/tests/cases/fourslash/externalModuleNameCompletions2.ts new file mode 100644 index 0000000000000..78198c316d4db --- /dev/null +++ b/tests/cases/fourslash/externalModuleNameCompletions2.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: ambients.ts +////declare module "a" {} +////declare module "b" {} + +// @Filename: moduleC.ts +////export var x = 0; + +// @Filename: global.ts +////var global = 0; + +// @Filename: current.ts +////declare module "d" { +//// import * from "/*1*/"; +////} + +goTo.marker("1"); +verify.memberListContains("a"); +verify.memberListContains("b"); +verify.memberListCount(2); + diff --git a/tests/cases/fourslash/externalModuleNameCompletions3.ts b/tests/cases/fourslash/externalModuleNameCompletions3.ts new file mode 100644 index 0000000000000..04c3af872dcda --- /dev/null +++ b/tests/cases/fourslash/externalModuleNameCompletions3.ts @@ -0,0 +1,24 @@ +/// + +// @Filename: moduleA.ts +////export var x = 0; + +// @Filename: folderA/moduleA.ts +////export var x = 0; + +// @Filename: folderA/folderAA/moduleAA.ts +////export var x = 0; + +// @Filename: folderA/folderAA/folderAAA/moduleAAA.ts +////export var x = 0; + +// @Filename: folderA/folderAA/folderAAB/current.ts +////import * from "/*1*/"; + +goTo.marker("1"); +verify.memberListContains("../folderAAA/moduleAAA"); +verify.memberListContains("../moduleAA"); +verify.memberListContains("../../moduleA"); +verify.memberListContains("../../../moduleA"); +verify.memberListCount(4); +