diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 4e03273537a..7c68306211a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1097,7 +1097,9 @@ namespace ts { return path.replace(/\\/g, "/"); } - // Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files") + /** + * Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files") + */ export function getRootLength(path: string): number { if (path.charCodeAt(0) === CharacterCodes.slash) { if (path.charCodeAt(1) !== CharacterCodes.slash) return 1; @@ -1126,9 +1128,14 @@ namespace ts { return 0; } + /** + * Internally, we represent paths as strings with '/' as the directory separator. + * When we make system calls (eg: LanguageServiceHost.getDirectory()), + * we expect the host to correctly handle paths in our specified format. + */ export const directorySeparator = "/"; const directorySeparatorCharCode = CharacterCodes.slash; - function getNormalizedParts(normalizedSlashedPath: string, rootLength: number) { + function getNormalizedParts(normalizedSlashedPath: string, rootLength: number): string[] { const parts = normalizedSlashedPath.substr(rootLength).split(directorySeparator); const normalized: string[] = []; for (const part of parts) { @@ -1168,6 +1175,11 @@ namespace ts { return path.charCodeAt(path.length - 1) === directorySeparatorCharCode; } + /** + * Returns the path except for its basename. Eg: + * + * /path/to/file.ext -> /path/to + */ export function getDirectoryPath(path: Path): Path; export function getDirectoryPath(path: string): string; export function getDirectoryPath(path: string): any { diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 5b33770ce28..dedbb14c929 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -171,12 +171,16 @@ namespace ts.server { return this.host.fileExists(path); } + readFile(fileName: string): string { + return this.host.readFile(fileName); + } + directoryExists(path: string): boolean { return this.host.directoryExists(path); } - readFile(fileName: string): string { - return this.host.readFile(fileName); + readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] { + return this.host.readDirectory(path, extensions, exclude, include); } getDirectories(path: string): string[] { diff --git a/src/services/completions.ts b/src/services/completions.ts index 3fff0df5f9e..c8d7c019e54 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -325,15 +325,28 @@ namespace ts.Completions { return result; } + /** + * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename. + */ function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] { + if (fragment === undefined) { + fragment = ""; + } + + fragment = normalizeSlashes(fragment); + + /** + * Remove the basename from the path. Note that we don't use the basename to filter completions; + * the client is responsible for refining completions. + */ fragment = getDirectoryPath(fragment); - if (!fragment) { - fragment = "./"; - } - else { - fragment = ensureTrailingDirectorySeparator(fragment); + + if (fragment === "") { + fragment = "." + directorySeparator; } + fragment = ensureTrailingDirectorySeparator(fragment); + const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); const baseDirectory = getDirectoryPath(absolutePath); const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); @@ -343,6 +356,12 @@ namespace ts.Completions { const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/undefined, /*include*/["./*"]); if (files) { + /** + * Multiple file entries might map to the same truncated name once we remove extensions + * (happens iff includeExtensions === false)so we use a set-like data structure. Eg: + * + * both foo.ts and foo.tsx become foo + */ const foundFiles = createMap(); for (let filePath of files) { filePath = normalizePath(filePath); @@ -539,36 +558,44 @@ namespace ts.Completions { return undefined; } + const completionInfo: CompletionInfo = { + /** + * We don't want the editor to offer any other completions, such as snippets, inside a comment. + */ + isGlobalCompletion: false, + isMemberCompletion: false, + /** + * The user may type in a path that doesn't yet exist, creating a "new identifier" + * with respect to the collection of identifiers the server is aware of. + */ + isNewIdentifierLocation: true, + + entries: [] + }; + const text = sourceFile.text.substr(range.pos, position - range.pos); const match = tripleSlashDirectiveFragmentRegex.exec(text); + if (match) { const prefix = match[1]; const kind = match[2]; const toComplete = match[3]; const scriptPath = getDirectoryPath(sourceFile.path); - let entries: CompletionEntry[]; if (kind === "path") { // Give completions for a relative path const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); + completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); } else { // Give completions based on the typings available const span: TextSpan = { start: range.pos + prefix.length, length: match[0].length - prefix.length }; - entries = getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span); + completionInfo.entries = getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span); } - - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: true, - entries - }; } - return undefined; + return completionInfo; } function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] { @@ -1674,9 +1701,15 @@ namespace ts.Completions { * Matches a triple slash reference directive with an incomplete string literal for its path. Used * to determine if the caret is currently within the string literal and capture the literal fragment * for completions. - * For example, this matches /// - -// Should give completions for ts files when allowJs is false - -// @Filename: test0.ts -//// import * as foo1 from "./*import_as0*/ -//// import * as foo2 from ".//*import_as1*/ -//// import * as foo4 from "./folder//*import_as2*/ - -//// import foo6 = require("./*import_equals0*/ -//// import foo7 = require(".//*import_equals1*/ -//// import foo9 = require("./folder//*import_equals2*/ - -//// var foo11 = require("./*require0*/ -//// var foo12 = require(".//*require1*/ -//// var foo14 = require("./folder//*require2*/ - -// @Filename: parentTest/sub/test5.ts -//// import * as foo16 from "../g/*import_as3*/ -//// import foo17 = require("../g/*import_equals3*/ -//// var foo18 = require("../g/*require3*/ - - -// @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/f3.ts -//// /*subf1*/ -// @Filename: folder/h1.ts -//// /*subh1*/ -// @Filename: parentTest/f4.ts -//// /*parentf1*/ -// @Filename: parentTest/g1.ts -//// /*parentg1*/ -const kinds = ["import_as", "import_equals", "require"]; - -for (const kind of kinds) { - goTo.marker(kind + "0"); - verify.completionListIsEmpty(); - - goTo.marker(kind + "1"); - verify.completionListContains("f1"); - verify.completionListContains("f2"); - verify.completionListContains("e1"); - verify.completionListContains("folder"); - verify.completionListContains("parentTest"); - verify.not.completionListItemsCountIsGreaterThan(5); - - goTo.marker(kind + "2"); - verify.completionListContains("f3"); - verify.completionListContains("h1"); - verify.not.completionListItemsCountIsGreaterThan(2); - - goTo.marker(kind + "3"); - verify.completionListContains("f4"); - verify.completionListContains("g1"); - verify.completionListContains("sub"); - verify.not.completionListItemsCountIsGreaterThan(3); -} \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSFalse.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSFalse.ts new file mode 100644 index 00000000000..f343ce8699b --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSFalse.ts @@ -0,0 +1,72 @@ +/// + +// Should give completions for ts files only when allowJs is false. + +// @Filename: test0.ts +//// import * as foo1 from "./*import_as0*/ +//// import * as foo2 from ".//*import_as1*/ +//// import * as foo4 from "./d1//*import_as2*/ + +//// import foo6 = require("./*import_equals0*/ +//// import foo7 = require(".//*import_equals1*/ +//// import foo9 = require("./d1//*import_equals2*/ + +//// var foo11 = require("./*require0*/ +//// var foo12 = require(".//*require1*/ +//// var foo14 = require("./d1//*require2*/ + +// @Filename: d2/d3/test1.ts +//// import * as foo16 from "..//*import_as3*/ +//// import foo17 = require("..//*import_equals3*/ +//// var foo18 = require("..//*require3*/ + + +// @Filename: f1.ts +//// /*f1*/ +// @Filename: f2.js +//// /*f2*/ +// @Filename: f3.d.ts +//// /*f3*/ +// @Filename: f4.tsx +//// /f4*/ +// @Filename: f5.js +//// /*f5*/ +// @Filename: f6.jsx +//// /*f6*/ +// @Filename: f7.ts +//// /*f7*/ +// @Filename: d1/f8.ts +//// /*d1f1*/ +// @Filename: d1/f9.ts +//// /*d1f9*/ +// @Filename: d2/f10.ts +//// /*d2f1*/ +// @Filename: d2/f11.ts +//// /*d2f11*/ + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListIsEmpty(); + + goTo.marker(kind + "1"); + verify.completionListContains("f1"); + verify.completionListContains("f3"); + verify.completionListContains("f4"); + verify.completionListContains("f7"); + verify.completionListContains("d1"); + verify.completionListContains("d2"); + verify.not.completionListItemsCountIsGreaterThan(6); + + goTo.marker(kind + "2"); + verify.completionListContains("f8"); + verify.completionListContains("f9"); + verify.not.completionListItemsCountIsGreaterThan(2); + + goTo.marker(kind + "3"); + verify.completionListContains("f10"); + verify.completionListContains("f11"); + verify.completionListContains("d3"); + verify.not.completionListItemsCountIsGreaterThan(3); +} \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSTrue.ts similarity index 51% rename from tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts rename to tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSTrue.ts index 18a80ab3481..6eab9aaeca2 100644 --- a/tests/cases/fourslash/completionForStringLiteralRelativeImport2.ts +++ b/tests/cases/fourslash/completionForStringLiteralRelativeImportAllowJSTrue.ts @@ -1,6 +1,6 @@ /// -// Should give completions for ts and js files when allowJs is true +// Should give completions for ts and js files when allowJs is true. // @allowJs: true @@ -16,38 +16,33 @@ // @Filename: f1.ts //// /f1*/ -// @Filename: f1.js -//// /*f1j*/ -// @Filename: f1.d.ts -//// /*f1d*/ -// @Filename: f2.tsx +// @Filename: f2.js //// /*f2*/ -// @Filename: f3.js +// @Filename: f3.d.ts //// /*f3*/ -// @Filename: f4.jsx +// @Filename: f4.tsx //// /*f4*/ -// @Filename: e1.ts -//// /*e1*/ -// @Filename: e2.js -//// /*e2*/ +// @Filename: f5.js +//// /*f5*/ +// @Filename: f6.jsx +//// /*f6*/ +// @Filename: g1.ts +//// /*g1*/ +// @Filename: g2.js +//// /*g2*/ const kinds = ["import_as", "import_equals", "require"]; for (const kind of kinds) { - goTo.marker(kind + "0"); + for(let i = 0; i < 2; ++i) { + goTo.marker(kind + i); verify.completionListContains("f1"); verify.completionListContains("f2"); verify.completionListContains("f3"); verify.completionListContains("f4"); - verify.completionListContains("e1"); - verify.completionListContains("e2"); - verify.not.completionListItemsCountIsGreaterThan(6); - - goTo.marker(kind + "1"); - verify.completionListContains("f1"); - verify.completionListContains("f2"); - verify.completionListContains("f3"); - verify.completionListContains("f4"); - verify.completionListContains("e1"); - verify.completionListContains("e2"); - verify.not.completionListItemsCountIsGreaterThan(6); + verify.completionListContains("f5"); + verify.completionListContains("f6"); + verify.completionListContains("g1"); + verify.completionListContains("g2"); + verify.not.completionListItemsCountIsGreaterThan(8); + } } \ No newline at end of file diff --git a/tests/cases/fourslash/completionForTripleSlashReference1.ts b/tests/cases/fourslash/completionForTripleSlashReference1.ts deleted file mode 100644 index 2ce4de2e72e..00000000000 --- a/tests/cases/fourslash/completionForTripleSlashReference1.ts +++ /dev/null @@ -1,51 +0,0 @@ -/// - -// Should give completions for relative references to ts files when allowJs is false - -// @Filename: test0.ts -//// /// -//// /// -// Should give completions for absolute paths +// Exercises completions for absolute paths. // @Filename: tests/test0.ts //// /// + +// Exercises whether completions are supplied, conditional on the caret position in the ref comment. + +// @Filename: f.ts +//// /*f*/ + +// @Filename: test.ts +//// /// /*7*/ + +for(let m = 0; m < 8; ++m) { + goTo.marker("" + m); + verify.not.completionListItemsCountIsGreaterThan(0); +} + +for(let m of ["8", "9"]) { + goTo.marker(m); + verify.completionListContains("f.ts"); + verify.not.completionListItemsCountIsGreaterThan(1); +} \ No newline at end of file diff --git a/tests/cases/fourslash/tripleSlashRefPathCompletionExtensionsAllowJSFalse.ts b/tests/cases/fourslash/tripleSlashRefPathCompletionExtensionsAllowJSFalse.ts new file mode 100644 index 00000000000..95a4cd01b60 --- /dev/null +++ b/tests/cases/fourslash/tripleSlashRefPathCompletionExtensionsAllowJSFalse.ts @@ -0,0 +1,31 @@ +/// + +// Should give completions for relative references to ts files only when allowJs is false. + +// @Filename: test0.ts +//// /// -// Should give completions for relative references to js and ts files when allowJs is true +// Should give completions for relative references to ts and js files when allowJs is true. // @allowJs: true // @Filename: test0.ts //// /// +//// /// + +// Exercises completions for hidden files (ie: those beginning with '.') + +// @Filename: f.ts +//// /*f*/ +// @Filename: .hidden.ts +//// /*hidden*/ + +// @Filename: test.ts +//// /// + +// Exercises how changes in the basename change the completions offered. +// They should have no effect, as filtering completions is the responsibility of the editor. + +// @Filename: f1.ts +//// /*f1*/ +// @Filename: f2.ts +//// /*f2*/ +// @Filename: d/g.ts +//// /*g*/ + +// @Filename: test.ts +//// /// + +// Exercises relative path completions going up and down 2 directories +// and the use of forward- and back-slashes and combinations thereof. + +// @Filename: f.ts +//// /*f1*/ +// @Filename: d1/g.ts +//// /*g1*/ +// @Filename: d1/d2/h.ts +//// /*h1*/ +// @Filename: d1/d2/d3/i.ts +//// /*i1*/ +// @Filename: d1/d2/d3/d4/j.ts +//// /*j1*/ + +// @Filename: d1/d2/test.ts +//// ///