diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0ca0fd907fb..14cab98db0d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2,6 +2,8 @@ /* @internal */ namespace ts { + const ambientModuleSymbolRegex = /^".+"$/; + let nextSymbolId = 1; let nextNodeId = 1; let nextMergeId = 1; @@ -101,6 +103,7 @@ namespace ts { getAliasedSymbol: resolveAlias, getEmitResolver, getExportsOfModule: getExportsOfModuleAsArray, + getAmbientModules, getJsxElementAttributesType, getJsxIntrinsicTagNames, @@ -19104,5 +19107,15 @@ namespace ts { return true; } } + + function getAmbientModules(): Symbol[] { + const result: Symbol[] = []; + for (const sym in globals) { + if (globals.hasOwnProperty(sym) && ambientModuleSymbolRegex.test(sym)) { + result.push(globals[sym]); + } + } + return result; + } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index a01f3a4c5b2..2e5a5950098 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -15,9 +15,9 @@ namespace ts { const defaultTypeRoots = ["node_modules/@types"]; - export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean): string { + export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName="tsconfig.json"): string { while (true) { - const fileName = combinePaths(searchPath, "tsconfig.json"); + const fileName = combinePaths(searchPath, configName); if (fileExists(fileName)) { return fileName; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index cac04172a5c..c2cf3a8527f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1854,6 +1854,7 @@ namespace ts { getJsxElementAttributesType(elementNode: JsxOpeningLikeElement): Type; getJsxIntrinsicTagNames(): Symbol[]; isOptionalParameter(node: ParameterDeclaration): boolean; + getAmbientModules(): Symbol[]; // Should not be called directly. Should only be accessed through the Program instance. /* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 1977d95492c..ace211f2553 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -742,14 +742,14 @@ namespace Harness { } export function readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[]) { - const fs = new Utils.VirtualFileSystem(path, useCaseSensitiveFileNames()); + const fs = new Utils.VirtualFileSystem(path, useCaseSensitiveFileNames()); for (const file in listFiles(path)) { fs.addFile(file); } return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), getCurrentDirectory(), path => { const entry = fs.traversePath(path); if (entry && entry.isDirectory()) { - const directory = entry; + const directory = >entry; return { files: ts.map(directory.getFiles(), f => f.name), directories: ts.map(directory.getDirectories(), d => d.name) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 8567a9109de..682496442e9 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -123,7 +123,7 @@ namespace Harness.LanguageService { } export class LanguageServiceAdapterHost { - protected fileNameToScript: ts.Map = {}; + protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(/*root*/"c:", /*useCaseSensitiveFilenames*/false); constructor(protected cancellationToken = DefaultHostCancellationToken.Instance, protected settings = ts.getDefaultCompilerOptions()) { @@ -135,7 +135,8 @@ namespace Harness.LanguageService { public getFilenames(): string[] { const fileNames: string[] = []; - ts.forEachValue(this.fileNameToScript, (scriptInfo) => { + this.virtualFileSystem.getAllFileEntries().forEach((virtualEntry) => { + const scriptInfo = virtualEntry.content; if (scriptInfo.isRootFile) { // only include root files here // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. @@ -146,11 +147,12 @@ namespace Harness.LanguageService { } public getScriptInfo(fileName: string): ScriptInfo { - return ts.lookUp(this.fileNameToScript, fileName); + const fileEntry = this.virtualFileSystem.traversePath(fileName); + return fileEntry && fileEntry.isFile() ? (>fileEntry).content : undefined; } public addScript(fileName: string, content: string, isRootFile: boolean): void { - this.fileNameToScript[fileName] = new ScriptInfo(fileName, content, isRootFile); + this.virtualFileSystem.addFile(fileName, new ScriptInfo(fileName, content, isRootFile)); } public editScript(fileName: string, start: number, end: number, newText: string) { @@ -171,7 +173,7 @@ namespace Harness.LanguageService { * @param col 0 based index */ public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { - const script: ScriptInfo = this.fileNameToScript[fileName]; + const script: ScriptInfo = this.getScriptInfo(fileName); assert.isOk(script); return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); @@ -182,7 +184,13 @@ namespace Harness.LanguageService { class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost { getCompilationSettings() { return this.settings; } getCancellationToken() { return this.cancellationToken; } - getDirectories(path: string): string[] { return []; } + getDirectories(path: string): string[] { + const dir = this.virtualFileSystem.traversePath(path); + if (dir && dir.isDirectory()) { + return ts.map((>dir).getDirectories(), (d) => ts.combinePaths(path, d.name)); + } + return []; + } getCurrentDirectory(): string { return ""; } getDefaultLibFileName(): string { return Harness.Compiler.defaultLibFileName; } getScriptFileNames(): string[] { return this.getFilenames(); } @@ -196,6 +204,39 @@ namespace Harness.LanguageService { return script ? script.version.toString() : undefined; } + fileExists(fileName: string): boolean { + const script = this.getScriptSnapshot(fileName); + return script !== undefined; + } + readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] { + return ts.matchFiles(path, extensions, exclude, include, + /*useCaseSensitiveFileNames*/false, + /*currentDirectory*/"/", + (p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p)); + } + readFile(path: string, encoding?: string): string { + const snapshot = this.getScriptSnapshot(path); + return snapshot.getText(0, snapshot.getLength()); + } + resolvePath(path: string): string { + // Reduce away "." and ".." + const parts = path.split("/"); + const res: string[] = []; + for (let i = 0; i < parts.length; i++) { + if (parts[i] === ".") { + continue; + } + else if (parts[i] === ".." && res.length > 0) { + res.splice(res.length - 1, 1); + } + else { + res.push(parts[i]); + } + } + return res.join("/"); + } + + log(s: string): void { } trace(s: string): void { } error(s: string): void { } @@ -299,6 +340,9 @@ namespace Harness.LanguageService { const snapshot = this.nativeHost.getScriptSnapshot(fileName); return snapshot && snapshot.getText(0, snapshot.getLength()); } + resolvePath(path: string): string { + return this.nativeHost.resolvePath(path); + } log(s: string): void { this.nativeHost.log(s); } trace(s: string): void { this.nativeHost.trace(s); } error(s: string): void { this.nativeHost.error(s); } diff --git a/src/harness/virtualFileSystem.ts b/src/harness/virtualFileSystem.ts index 30192b8b8ec..5a89efea7fa 100644 --- a/src/harness/virtualFileSystem.ts +++ b/src/harness/virtualFileSystem.ts @@ -1,11 +1,11 @@ /// /// namespace Utils { - export class VirtualFileSystemEntry { - fileSystem: VirtualFileSystem; + export class VirtualFileSystemEntry { + fileSystem: VirtualFileSystem; name: string; - constructor(fileSystem: VirtualFileSystem, name: string) { + constructor(fileSystem: VirtualFileSystem, name: string) { this.fileSystem = fileSystem; this.name = name; } @@ -15,15 +15,15 @@ namespace Utils { isFileSystem() { return false; } } - export class VirtualFile extends VirtualFileSystemEntry { - content: string; + export class VirtualFile extends VirtualFileSystemEntry { + content: T; isFile() { return true; } } - export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry { - abstract getFileSystemEntries(): VirtualFileSystemEntry[]; + export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry { + abstract getFileSystemEntries(): VirtualFileSystemEntry[]; - getFileSystemEntry(name: string): VirtualFileSystemEntry { + getFileSystemEntry(name: string): VirtualFileSystemEntry { for (const entry of this.getFileSystemEntries()) { if (this.fileSystem.sameName(entry.name, name)) { return entry; @@ -32,57 +32,57 @@ namespace Utils { return undefined; } - getDirectories(): VirtualDirectory[] { - return ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory()); + getDirectories(): VirtualDirectory[] { + return []>ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory()); } - getFiles(): VirtualFile[] { - return ts.filter(this.getFileSystemEntries(), entry => entry.isFile()); + getFiles(): VirtualFile[] { + return []>ts.filter(this.getFileSystemEntries(), entry => entry.isFile()); } - getDirectory(name: string): VirtualDirectory { + getDirectory(name: string): VirtualDirectory { const entry = this.getFileSystemEntry(name); - return entry.isDirectory() ? entry : undefined; + return entry.isDirectory() ? >entry : undefined; } - getFile(name: string): VirtualFile { + getFile(name: string): VirtualFile { const entry = this.getFileSystemEntry(name); - return entry.isFile() ? entry : undefined; + return entry.isFile() ? >entry : undefined; } } - export class VirtualDirectory extends VirtualFileSystemContainer { - private entries: VirtualFileSystemEntry[] = []; + export class VirtualDirectory extends VirtualFileSystemContainer { + private entries: VirtualFileSystemEntry[] = []; isDirectory() { return true; } getFileSystemEntries() { return this.entries.slice(); } - addDirectory(name: string): VirtualDirectory { + addDirectory(name: string): VirtualDirectory { const entry = this.getFileSystemEntry(name); if (entry === undefined) { - const directory = new VirtualDirectory(this.fileSystem, name); + const directory = new VirtualDirectory(this.fileSystem, name); this.entries.push(directory); return directory; } else if (entry.isDirectory()) { - return entry; + return >entry; } else { return undefined; } } - addFile(name: string, content?: string): VirtualFile { + addFile(name: string, content?: T): VirtualFile { const entry = this.getFileSystemEntry(name); if (entry === undefined) { - const file = new VirtualFile(this.fileSystem, name); + const file = new VirtualFile(this.fileSystem, name); file.content = content; this.entries.push(file); return file; } else if (entry.isFile()) { - const file = entry; + const file = >entry; file.content = content; return file; } @@ -92,8 +92,8 @@ namespace Utils { } } - export class VirtualFileSystem extends VirtualFileSystemContainer { - private root: VirtualDirectory; + export class VirtualFileSystem extends VirtualFileSystemContainer { + private root: VirtualDirectory; currentDirectory: string; useCaseSensitiveFileNames: boolean; @@ -101,7 +101,7 @@ namespace Utils { constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) { super(undefined, ""); this.fileSystem = this; - this.root = new VirtualDirectory(this, ""); + this.root = new VirtualDirectory(this, ""); this.currentDirectory = currentDirectory; this.useCaseSensitiveFileNames = useCaseSensitiveFileNames; } @@ -112,7 +112,7 @@ namespace Utils { addDirectory(path: string) { const components = ts.getNormalizedPathComponents(path, this.currentDirectory); - let directory: VirtualDirectory = this.root; + let directory: VirtualDirectory = this.root; for (const component of components) { directory = directory.addDirectory(component); if (directory === undefined) { @@ -123,7 +123,7 @@ namespace Utils { return directory; } - addFile(path: string, content?: string) { + addFile(path: string, content?: T) { const absolutePath = ts.getNormalizedAbsolutePath(path, this.currentDirectory); const fileName = ts.getBaseFileName(path); const directoryPath = ts.getDirectoryPath(absolutePath); @@ -141,14 +141,14 @@ namespace Utils { } traversePath(path: string) { - let directory: VirtualDirectory = this.root; + let directory: VirtualDirectory = this.root; for (const component of ts.getNormalizedPathComponents(path, this.currentDirectory)) { const entry = directory.getFileSystemEntry(component); if (entry === undefined) { return undefined; } else if (entry.isDirectory()) { - directory = entry; + directory = >entry; } else { return entry; @@ -157,9 +157,34 @@ namespace Utils { return directory; } + + getAccessibleFileSystemEntries(path: string) { + const entry = this.traversePath(path); + if (entry && entry.isDirectory()) { + const directory = >entry; + return { + files: ts.map(directory.getFiles(), f => f.name), + directories: ts.map(directory.getDirectories(), d => d.name) + }; + } + return { files: [], directories: [] }; + } + + getAllFileEntries() { + const fileEntries: VirtualFile[] = []; + getFilesRecursive(this.root, fileEntries); + return fileEntries; + + function getFilesRecursive(dir: VirtualDirectory, result: VirtualFile[]) { + dir.getFiles().forEach((e) => result.push(e)); + dir.getDirectories().forEach((subDir) => getFilesRecursive(subDir, result)); + } + } + + } - export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost { + export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost { constructor(currentDirectory: string, ignoreCase: boolean, files: string[]) { super(currentDirectory, ignoreCase); for (const file of files) { @@ -170,17 +195,5 @@ namespace Utils { readDirectory(path: string, extensions: string[], excludes: string[], includes: string[]) { return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, (path: string) => this.getAccessibleFileSystemEntries(path)); } - - getAccessibleFileSystemEntries(path: string) { - const entry = this.traversePath(path); - if (entry && entry.isDirectory()) { - const directory = entry; - return { - files: ts.map(directory.getFiles(), f => f.name), - directories: ts.map(directory.getDirectories(), d => d.name) - }; - } - return { files: [], directories: [] }; - } } } \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9c0662e5534..b94c366e7c9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -321,6 +321,14 @@ namespace ts.server { return this.host.getDirectories(path); } + readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] { + return this.host.readDirectory(path, extensions, exclude, include); + } + + readFile(path: string, encoding?: string): string { + return this.host.readFile(path, encoding); + } + /** * @param line 1 based index */ diff --git a/src/services/services.ts b/src/services/services.ts index 492be6f1719..c6c8a7a2e83 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1070,6 +1070,11 @@ namespace ts { error?(s: string): void; useCaseSensitiveFileNames?(): boolean; + readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[]; + resolvePath(path: string): string; + readFile(path: string, encoding?: string): string; + fileExists(path: string): boolean; + /* * LS host can optionally implement this method if it wants to be completely in charge of module name resolution. * if implementation is omitted then language service will use built-in module resolution logic and get answers to @@ -1943,6 +1948,9 @@ namespace ts { } + const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); + const tripleSlashDirectivePrefixRegex = /^\/\/\/\s*node); + } else { // Otherwise, get the completions from the contextual type if one exists return getStringLiteralCompletionEntriesFromContextualType(node); @@ -4314,6 +4339,258 @@ namespace ts { } } } + + function getStringLiteralCompletionEntriesFromModuleNames(node: StringLiteral) { + const literalValue = node.text; + let result: CompletionEntry[]; + + const isRelativePath = startsWith(literalValue, "."); + const scriptDir = getDirectoryPath(node.getSourceFile().path); + if (isRelativePath || isRootedDiskPath(literalValue)) { + result = getCompletionEntriesForDirectoryFragment(literalValue, scriptDir, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/false); + } + else { + // Check for node modules + result = getCompletionEntriesForNonRelativeModules(literalValue, scriptDir); + } + + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: result + }; + } + + function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean): CompletionEntry[] { + // Complete the path by looking for source files and directories + const result: CompletionEntry[] = []; + + const toComplete = getBaseFileName(fragment); + const absolutePath = normalizeSlashes(host.resolvePath(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment))); + const baseDir = getDirectoryPath(absolutePath); + + + if (directoryProbablyExists(baseDir, host)) { + // Enumerate the available files + const files = host.readDirectory(baseDir, extensions, /*exclude*/undefined, /*include*/["./*"]); + files.forEach((f) => { + const fName = includeExtensions ? getBaseFileName(f) : removeFileExtension(getBaseFileName(f)); + + if (startsWith(fName, toComplete)) { + result.push({ + name: fName, + kind: ScriptElementKind.unknown, + kindModifiers: ScriptElementKindModifier.none, + sortText: fName + }); + } + }); + + // If possible, get folder completion as well + if (host.getDirectories) { + const directories = host.getDirectories(baseDir); + directories.forEach((d) => { + const dName = getBaseFileName(removeTrailingDirectorySeparator(d)); + + if (startsWith(dName, toComplete)) { + result.push({ + name: ensureTrailingDirectorySeparator(dName), + kind: ScriptElementKind.unknown, + kindModifiers: ScriptElementKindModifier.none, + sortText: dName + }); + } + }); + } + } + + return includeExtensions ? result : deduplicate(result, (a, b) => a.name === b.name); + } + + /** + * Check all of the declared modules and those in node modules. Possible sources of modules: + * Modules declared in the program + * Modules from node_modules (i.e. those listed in package.json) + * This includes all files that are found in node_modules/moduleName/ and node_modules/@types/moduleName/ + * with acceptable file extensions + */ + function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string): CompletionEntry[] { + return ts.map(enumeratePotentialNonRelativeModules(fragment, scriptPath), (moduleName) => { + return { + name: moduleName, + kind: ScriptElementKind.unknown, + kindModifiers: ScriptElementKindModifier.none, + sortText: moduleName + }; + }); + } + + function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string) { + const ambientSymbolNameRegex = /^"(.+?)"$/; + + // If this is a nested module, get the module name + const firstSeparator = fragment.indexOf(directorySeparator); + const moduleNameFragment = firstSeparator !== -1 ? fragment.substr(0, firstSeparator) : fragment; + const isNestedModule = fragment !== moduleNameFragment; + + // Get modules that the type checker picked up + const ambientModules = ts.map(program.getTypeChecker().getAmbientModules(), (sym) => { + const match = ambientSymbolNameRegex.exec(sym.name); + if (match) { + return match[1]; + } + // This should never happen + return sym.name; + }); + let nonRelativeModules = ts.filter(ambientModules, (moduleName) => startsWith(moduleName, fragment)); + + // Nested modules of the form "module-name/sub" need to be adjusted to only return the string + // after the last '/' that appears in the fragment because editors insert the completion + // only after that character + nonRelativeModules = ts.map(nonRelativeModules, (moduleName) => { + if (moduleName.indexOf(directorySeparator) !== -1) { + if (isNestedModule) { + return moduleName.substr(fragment.lastIndexOf(directorySeparator) + 1); + } + } + return moduleName; + }); + + // Check for node_modules. We only offer completions for modules that are listed in the + // package.json for a project for efficiency and to ensure that the completion list is + // not polluted with sub-dependencies + findPackageJsons(scriptPath).forEach((packageJson) => { + const package = tryReadingPackageJson(packageJson); + if (!package) { + return; + } + + const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); + const foundModuleNames: string[] = []; + + if (package.dependencies) { + addPotentialPackageNames(package.dependencies, moduleNameFragment, foundModuleNames); + } + if (package.devDependencies) { + addPotentialPackageNames(package.devDependencies, moduleNameFragment, foundModuleNames); + } + + foundModuleNames.forEach((moduleName) => { + if (isNestedModule && moduleName === moduleNameFragment) { + const moduleDir = combinePaths(nodeModulesDir, moduleName); + if (directoryProbablyExists(moduleDir, host)) { + // Get all possible nested module names from files with all extensions + const nestedFiles = host.readDirectory(moduleDir, allSupportedExtensions, /*exclude*/undefined, /*include*/["./*"]); + + // Add those with typings to the completion list + nestedFiles.forEach((f) => { + const nestedModule = removeFileExtension(getBaseFileName(f)); + if (hasTypeScriptFileExtension(f)) { + nonRelativeModules.push(nestedModule); + } + }); + } + } + else if (startsWith(moduleName, fragment)) { + if (moduleCanBeImported(combinePaths(nodeModulesDir, moduleName))) { + nonRelativeModules.push(moduleName); + } + else { + nonRelativeModules.push(ensureTrailingDirectorySeparator(moduleName)); + } + } + }); + }); + + return deduplicate(nonRelativeModules); + } + + function findPackageJsons(currentDir: string): string[] { + const paths: string[] = []; + let currentConfigPath: string; + while (true) { + currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json"); + if (currentConfigPath) { + paths.push(currentConfigPath); + + currentDir = getDirectoryPath(currentConfigPath); + const parent = getDirectoryPath(currentDir); + if (currentDir === parent) { + break; + } + currentDir = parent; + } + else { + break; + } + } + + return paths; + } + + function tryReadingPackageJson(filePath: string) { + try { + const fileText = host.readFile(filePath); + return JSON.parse(fileText); + } + catch (e) { + return undefined; + } + } + + function addPotentialPackageNames(dependencies: any, prefix: string, result: string[]) { + for (const dep in dependencies) { + if (dependencies.hasOwnProperty(dep) && startsWith(dep, prefix)) { + result.push(dep); + } + } + } + + /* + * A module can be imported by name alone if one of the following is true: + * It defines the "typings" property in its package.json + * The module has a "main" export and an index.d.ts file + * The module has an index.ts + */ + function moduleCanBeImported(modulePath: string): boolean { + const packagePath = combinePaths(modulePath, "package.json"); + + let hasMainExport = false; + if (host.fileExists(packagePath)) { + const package = tryReadingPackageJson(packagePath); + if (package) { + if (package.typings) { + return true; + } + hasMainExport = !!package.main; + } + } + + hasMainExport = hasMainExport || host.fileExists(combinePaths(modulePath, "index.js")); + + return (hasMainExport && host.fileExists(combinePaths(modulePath, "index.d.ts"))) || host.fileExists(combinePaths(modulePath, "index.ts")); + } + + function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number) { + const node = getTokenAtPosition(sourceFile, position); + if (!node) { + return undefined; + } + + const text = sourceFile.text.substr(node.pos, position); + const match = tripleSlashDirectiveFragmentRegex.exec(text); + if (match) { + const fragment = match[1]; + const scriptPath = getDirectoryPath(sourceFile.path); + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: getCompletionEntriesForDirectoryFragment(fragment, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true) + }; + } + + return undefined; + } } function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { @@ -6189,7 +6466,6 @@ namespace ts { symbolToIndex: number[]): void { const sourceFile = container.getSourceFile(); - const tripleSlashDirectivePrefixRegex = /^\/\/\/\s* + +// @Filename: tests/test0.ts +//// import * as foo from "f/*0*/ + +// @Filename: tests/test1.ts +//// import * as foo from "fake-module//*1*/ + +// @Filename: tests/test2.ts +//// import * as foo from "fake-module/*2*/ + +// @Filename: package.json +//// { "dependencies": { "fake-module": "latest" }, "devDependencies": { "fake-module-dev": "latest" } } + +// @Filename: node_modules/fake-module/index.js +//// /*fake-module*/ +// @Filename: node_modules/fake-module/index.d.ts +//// /*fakemodule-d-ts*/ +// @Filename: node_modules/fake-module/ts.ts +//// /*ts*/ +// @Filename: node_modules/fake-module/dts.d.ts +//// /*dts*/ +// @Filename: node_modules/fake-module/tsx.tsx +//// /*tsx*/ +// @Filename: node_modules/fake-module/js.js +//// /*js*/ +// @Filename: node_modules/fake-module/jsx.jsx +//// /*jsx*/ + +// @Filename: node_modules/fake-module-dev/index.js +//// /*fakemodule-dev*/ +// @Filename: node_modules/fake-module-dev/index.d.ts +//// /*fakemodule-dev-d-ts*/ + +// @Filename: node_modules/unlisted-module/index.ts +//// /*unlisted-module*/ + +goTo.marker("0"); +verify.completionListContains("fake-module"); +verify.completionListContains("fake-module-dev"); +verify.not.completionListItemsCountIsGreaterThan(2); + +goTo.marker("1"); +verify.completionListContains("index"); +verify.completionListContains("ts"); +verify.completionListContains("dts"); +verify.completionListContains("tsx"); +verify.not.completionListItemsCountIsGreaterThan(4); + +goTo.marker("2"); +verify.completionListContains("fake-module"); +verify.completionListContains("fake-module-dev"); +verify.not.completionListItemsCountIsGreaterThan(2); \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts new file mode 100644 index 00000000000..c3e6f2e9062 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts @@ -0,0 +1,32 @@ +/// + +// @Filename: tests/test0.ts +//// import * as foo from "fake-module//*0*/ + +// @Filename: package.json +//// { "dependencies": { "fake-module": "latest" }, "devDependencies": { "fake-module-dev": "latest" } } + +// @Filename: node_modules/fake-module/repeated.ts +//// /*repeatedts*/ +// @Filename: node_modules/fake-module/repeated.tsx +//// /*repeatedtsx*/ +// @Filename: node_modules/fake-module/repeated.d.ts +//// /*repeateddts*/ +// @Filename: node_modules/fake-module/other.js +//// /*other*/ +// @Filename: node_modules/fake-module/other2.js +//// /*other2*/ + +// @Filename: node_modules/unlisted-module/index.js +//// /*unlisted-module*/ + +// @Filename: node_modules/@types/fake-module/other.d.ts +//// declare module "fake-module/other" {} + +// @Filename: node_modules/@types/unlisted-module/index.d.ts +//// /*unlisted-types*/ + +goTo.marker("0"); +verify.completionListContains("repeated"); +verify.completionListContains("other"); +verify.not.completionListItemsCountIsGreaterThan(2); diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport3.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport3.ts new file mode 100644 index 00000000000..7be26cf76d2 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport3.ts @@ -0,0 +1,30 @@ +/// +// @allowJs: true + +// @Filename: tests/test0.ts +//// import * as foo from "fake-module//*0*/ + +// @Filename: package.json +//// { "dependencies": { "fake-module": "latest" } } + +// @Filename: node_modules/fake-module/ts.ts +//// /*ts*/ +// @Filename: node_modules/fake-module/tsx.tsx +//// /*tsx*/ +// @Filename: node_modules/fake-module/dts.d.ts +//// /*dts*/ +// @Filename: node_modules/fake-module/js.js +//// /*js*/ +// @Filename: node_modules/fake-module/jsx.jsx +//// /*jsx*/ +// @Filename: node_modules/fake-module/repeated.js +//// /*repeatedjs*/ +// @Filename: node_modules/fake-module/repeated.jsx +//// /*repeatedjsx*/ + +goTo.marker("0"); + +verify.completionListContains("ts"); +verify.completionListContains("tsx"); +verify.completionListContains("dts"); +verify.not.completionListItemsCountIsGreaterThan(3); diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts new file mode 100644 index 00000000000..baa13638569 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts @@ -0,0 +1,45 @@ +/// + +// @Filename: dir1/dir2/dir3/dir4/test0.ts +//// import * as foo from "f/*0*/ + +// @Filename: dir1/dir2/dir3/dir4/test1.ts +//// import * as foo from "a/*1*/ + +// @Filename: dir1/dir2/dir3/dir4/test2.ts +//// import * as foo from "fake-module/*2*/ + +// @Filename: package.json +//// { "dependencies": { "fake-module": "latest" } } +// @Filename: node_modules/fake-module/ts.ts +//// /*module1*/ + +// @Filename: dir1/package.json +//// { "dependencies": { "fake-module2": "latest" } } +// @Filename: dir1/node_modules/@types/fake-module2/js.d.ts +//// declare module "ambient-module-test" {} + +// @Filename: dir1/dir2/dir3/package.json +//// { "dependencies": { "fake-module3": "latest" } } +// @Filename: dir1/dir2/dir3/node_modules/fake-module3/ts.ts +//// /*module3*/ + + +goTo.marker("0"); + +verify.completionListContains("fake-module/"); +verify.completionListContains("fake-module2/"); +verify.completionListContains("fake-module3/"); +verify.not.completionListItemsCountIsGreaterThan(3); + +goTo.marker("1"); + +verify.completionListContains("ambient-module-test"); +verify.not.completionListItemsCountIsGreaterThan(1); + +goTo.marker("2"); + +verify.completionListContains("fake-module/"); +verify.completionListContains("fake-module2/"); +verify.completionListContains("fake-module3/"); +verify.not.completionListItemsCountIsGreaterThan(3); diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport5.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport5.ts new file mode 100644 index 00000000000..a5bd602bbbf --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport5.ts @@ -0,0 +1,28 @@ +/// + +// @Filename: test0.ts +//// import * as foo from "/*0*/ + +// @Filename: test1.ts +//// import * as foo from "a/*1*/ + +// @Filename: ambientModules.d.ts +//// declare module "ambientModule" {} +//// declare module "otherAmbientModule" {} + +// @Filename: ambientModules2.d.ts +//// declare module "otherOtherAmbientModule" {} + + +goTo.marker("0"); + +verify.completionListContains("ambientModule"); +verify.completionListContains("otherAmbientModule"); +verify.completionListContains("otherOtherAmbientModule"); +verify.not.completionListItemsCountIsGreaterThan(3); + +goTo.marker("1"); + +verify.completionListContains("ambientModule"); +verify.not.completionListItemsCountIsGreaterThan(1); + diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport6.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport6.ts new file mode 100644 index 00000000000..ed588f35802 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport6.ts @@ -0,0 +1,57 @@ +/// + +// @Filename: tests/test0.ts +//// import * as foo from "module-/*0*/ + +// @Filename: package.json +//// { "dependencies": { +//// "module-no-main": "latest", +//// "module-no-main-index-d-ts": "latest", +//// "module-index-ts": "latest", +//// "module-index-d-ts-explicit-main": "latest", +//// "module-index-d-ts-default-main": "latest", +//// "module-typings": "latest" +//// } } + +// @Filename: node_modules/module-no-main/package.json +//// { } + +// @Filename: node_modules/module-no-main-index-d-ts/package.json +//// { } +// @Filename: node_modules/module-no-main-index-d-ts/index.d.ts +//// /*module-no-main-index-d-ts*/ + +// @Filename: node_modules/module-index-ts/package.json +//// { } +// @Filename: node_modules/module-index-ts/index.ts +//// /*module-index-ts*/ + +// @Filename: node_modules/module-index-d-ts-explicit-main/package.json +//// { "main":"./notIndex.js" } +// @Filename: node_modules/module-index-d-ts-explicit-main/notIndex.js +//// /*module-index-d-ts-explicit-main*/ +// @Filename: node_modules/module-index-d-ts-explicit-main/index.d.ts +//// /*module-index-d-ts-explicit-main2*/ + +// @Filename: node_modules/module-index-d-ts-default-main/package.json +//// { } +// @Filename: node_modules/module-index-d-ts-default-main/index.js +//// /*module-index-d-ts-default-main*/ +// @Filename: node_modules/module-index-d-ts-default-main/index.d.ts +//// /*module-index-d-ts-default-main2*/ + +// @Filename: node_modules/module-typings/package.json +//// { "typings":"./types.d.ts" } +// @Filename: node_modules/module-typings/types.d.ts +//// /*module-typings*/ + + +goTo.marker("0"); + +verify.completionListContains("module-no-main/"); +verify.completionListContains("module-no-main-index-d-ts/"); +verify.completionListContains("module-index-ts"); +verify.completionListContains("module-index-d-ts-explicit-main"); +verify.completionListContains("module-index-d-ts-default-main"); +verify.completionListContains("module-typings"); +verify.not.completionListItemsCountIsGreaterThan(6); diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImport1.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImport1.ts new file mode 100644 index 00000000000..26a6645b3fa --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralRelativeImport1.ts @@ -0,0 +1,77 @@ +/// + +// @Filename: test0.ts +//// import * as foo from "./*0*/ + +// @Filename: test1.ts +//// import * as foo from ".//*1*/ + +// @Filename: test2.ts +//// import * as foo from "./f/*2*/ + +// @Filename: test3.ts +//// import * as foo from "./folder//*3*/ + +// @Filename: test4.ts +//// import * as foo from "./folder/h/*4*/ + +// @Filename: parentTest/sub/test5.ts +//// import * as foo from "../g/*5*/ + +// @Filename: f1.ts +//// /*f1*/ +// @Filename: f1.js +//// /*f1j*/ +// @Filename: f1.d.ts +//// /*f1d*/ +// @Filename: f2.tsx +//// /f2*/ +// @Filename: f3.js +//// /*f3*/ +// @Filename: f4.jsx +//// /*f4*/ +// @Filename: e1.ts +//// /*e1*/ +// @Filename: folder/f1.ts +//// /*subf1*/ +// @Filename: folder/h1.ts +//// /*subh1*/ +// @Filename: parentTest/f1.ts +//// /*parentf1*/ +// @Filename: parentTest/g1.ts +//// /*parentg1*/ + +goTo.marker("0"); +verify.completionListIsEmpty(); + +goTo.marker("1"); +verify.completionListContains("f1"); +verify.completionListContains("f2"); +verify.completionListContains("e1"); +verify.completionListContains("test0"); +verify.completionListContains("test1"); +verify.completionListContains("test2"); +verify.completionListContains("test3"); +verify.completionListContains("test4"); +verify.completionListContains("folder/"); +verify.completionListContains("parentTest/"); +verify.not.completionListItemsCountIsGreaterThan(10); + +goTo.marker("2"); +verify.completionListContains("f1"); +verify.completionListContains("f2"); +verify.completionListContains("folder/"); +verify.not.completionListItemsCountIsGreaterThan(3); + +goTo.marker("3"); +verify.completionListContains("f1"); +verify.completionListContains("h1"); +verify.not.completionListItemsCountIsGreaterThan(2); + +goTo.marker("4"); +verify.completionListContains("h1"); +verify.not.completionListItemsCountIsGreaterThan(1); + +goTo.marker("5"); +verify.completionListContains("g1"); +verify.not.completionListItemsCountIsGreaterThan(1); \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts new file mode 100644 index 00000000000..1b4abe79119 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts @@ -0,0 +1,43 @@ +/// +// @allowJs: true + +// @Filename: test0.ts +//// import * as foo from ".//*0*/ + +// @Filename: test1.ts +//// import * as foo from "./f/*1*/ + +// @Filename: f1.ts +//// /f1*/ +// @Filename: f1.js +//// /*f1j*/ +// @Filename: f1.d.ts +//// /*f1d*/ +// @Filename: f2.tsx +//// /*f2*/ +// @Filename: f3.js +//// /*f3*/ +// @Filename: f4.jsx +//// /*f4*/ +// @Filename: e1.ts +//// /*e1*/ +// @Filename: e2.js +//// /*e2*/ + +goTo.marker("0"); +verify.completionListContains("f1"); +verify.completionListContains("f2"); +verify.completionListContains("f3"); +verify.completionListContains("f4"); +verify.completionListContains("e1"); +verify.completionListContains("e2"); +verify.completionListContains("test0"); +verify.completionListContains("test1"); +verify.not.completionListItemsCountIsGreaterThan(8); + +goTo.marker("1"); +verify.completionListContains("f1"); +verify.completionListContains("f2"); +verify.completionListContains("f3"); +verify.completionListContains("f4"); +verify.not.completionListItemsCountIsGreaterThan(4); \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts new file mode 100644 index 00000000000..1b10ffff5b6 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts @@ -0,0 +1,41 @@ +/// + +// @Filename: tests/test0.ts +//// import * as foo from "c:/tests/cases/f/*0*/ + +// @Filename: tests/test1.ts +//// import * as foo from "c:/tests/cases/fourslash/*1*/ + +// @Filename: tests/test2.ts +//// import * as foo from "c:/tests/cases/fourslash//*2*/ + +// @Filename: f1.ts +//// /*f1*/ +// @Filename: f2.tsx +//// /*f2*/ +// @Filename: folder/f1.ts +//// /*subf1*/ +// @Filename: f3.js +//// /*f3*/ +// @Filename: f4.jsx +//// /*f4*/ +// @Filename: e1.ts +//// /*e1*/ +// @Filename: e2.js +//// /*e2*/ + +goTo.marker("0"); +verify.completionListContains("fourslash/"); +verify.not.completionListItemsCountIsGreaterThan(1); + +goTo.marker("1"); +verify.completionListContains("fourslash/"); +verify.not.completionListItemsCountIsGreaterThan(1); + +goTo.marker("2"); +verify.completionListContains("f1"); +verify.completionListContains("f2"); +verify.completionListContains("e1"); +verify.completionListContains("folder/"); +verify.completionListContains("tests/"); +verify.not.completionListItemsCountIsGreaterThan(5); \ No newline at end of file diff --git a/tests/cases/fourslash/completionForTripleSlashReference1.ts b/tests/cases/fourslash/completionForTripleSlashReference1.ts new file mode 100644 index 00000000000..96582be1730 --- /dev/null +++ b/tests/cases/fourslash/completionForTripleSlashReference1.ts @@ -0,0 +1,79 @@ +/// + +// @Filename: test0.ts +//// /// + +// @Filename: test3.ts +//// /// +// @allowJs: true + +// @Filename: test0.ts +//// /// + +// @Filename: tests/test0.ts +//// ///