Merge pull request #11328 from Microsoft/FixTripleSlashCompletions

Fix triple slash completions
This commit is contained in:
Arthur Ozga 2016-10-06 16:39:35 -07:00 committed by GitHub
commit 94d895587a
17 changed files with 435 additions and 181 deletions

View File

@ -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 {

View File

@ -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[] {

View File

@ -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<boolean>();
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 /// <reference path="fragment
* For example, this matches
*
* /// <reference path="fragment
*
* but not
*
* /// <reference path="fragment"
*/
const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3]*)$/;
const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
interface VisibleModuleInfo {
moduleName: string;

View File

@ -505,7 +505,11 @@ namespace ts {
export interface CompletionInfo {
isGlobalCompletion: boolean;
isMemberCompletion: boolean;
isNewIdentifierLocation: boolean; // true when the current location also allows for a new identifier
/**
* true when the current location also allows for a new identifier
*/
isNewIdentifierLocation: boolean;
entries: CompletionEntry[];
}

View File

@ -1,70 +0,0 @@
/// <reference path='fourslash.ts' />
// 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);
}

View File

@ -0,0 +1,72 @@
/// <reference path='fourslash.ts' />
// 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);
}

View File

@ -1,6 +1,6 @@
/// <reference path='fourslash.ts' />
// 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);
}
}

View File

@ -1,51 +0,0 @@
/// <reference path='fourslash.ts' />
// Should give completions for relative references to ts files when allowJs is false
// @Filename: test0.ts
//// /// <reference path="/*0*/
//// /// <reference path="./*1*/
//// /// <reference path="../*2*/
//// /// <reference path=".//*3*/
//// /// <reference path="./f/*4*/" />
//// /// <reference path="./parentTest//*5*/
// @Filename: parentTest/sub/test1.ts
//// /// <reference path="../g/*6*/
// @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: parentTest/g1.ts
//// /*parentg1*/
for (let i = 0; i < 5; i++) {
goTo.marker("" + i);
verify.completionListContains("f1.ts");
verify.completionListContains("f1.d.ts");
verify.completionListContains("f2.tsx");
verify.completionListContains("e1.ts");
verify.completionListContains("parentTest");
verify.not.completionListItemsCountIsGreaterThan(5);
}
goTo.marker("5");
verify.completionListContains("g1.ts");
verify.completionListContains("sub");
verify.not.completionListItemsCountIsGreaterThan(2);
goTo.marker("6");
verify.completionListContains("g1.ts");
verify.completionListContains("sub");
verify.not.completionListItemsCountIsGreaterThan(2);

View File

@ -1,6 +1,6 @@
/// <reference path='fourslash.ts' />
// Should give completions for absolute paths
// Exercises completions for absolute paths.
// @Filename: tests/test0.ts
//// /// <reference path="/tests/cases/f/*0*/

View File

@ -0,0 +1,70 @@
/// <reference path='fourslash.ts' />
// Exercises completions for hidden files (ie: those beginning with '.')
// @Filename: f.ts
//// /*f1*/
// @Filename: d1/g.ts
//// /*g1*/
// @Filename: d1/d2/h.ts
//// /*h1*/
// @Filename: d1/d2/d3/i.ts
//// /// <reference path=".\..\..\/*28*/
// @Filename: test.ts
//// /// <reference path="/*0*/
//// /// <reference path=".//*1*/
//// /// <reference path=".\/*2*/
//// /// <reference path="./*3*/
//// /// <reference path="d1//*4*/
//// /// <reference path="d1/.//*5*/
//// /// <reference path="d1/.\/*6*/
//// /// <reference path="d1/./*7*/
//// /// <reference path="d1\/*8*/
//// /// <reference path="d1\.//*9*/
//// /// <reference path="d1\.\/*10*/
//// /// <reference path="d1\./*11*/
//// /// <reference path="d1/d2//*12*/
//// /// <reference path="d1/d2/.//*13*/
//// /// <reference path="d1/d2/.\/*14*/
//// /// <reference path="d1/d2/./*15*/
//// /// <reference path="d1/d2\/*16*/
//// /// <reference path="d1/d2\.//*17*/
//// /// <reference path="d1/d2\.\/*18*/
//// /// <reference path="d1/d2\./*19*/
//// /// <reference path="d1\d2//*20*/
//// /// <reference path="d1\d2/.//*21*/
//// /// <reference path="d1\d2/.\/*22*/
//// /// <reference path="d1\d2/./*23*/
//// /// <reference path="d1\d2\/*24*/
//// /// <reference path="d1\d2\.//*25*/
//// /// <reference path="d1\d2\.\/*26*/
//// /// <reference path="d1\d2\./*27*/
testBlock(0, 'f.ts', "d1");
testBlock(4, 'g.ts', "d2");
testBlock(8, 'g.ts', "d2");
testBlock(12, 'h.ts', "d3");
testBlock(16, 'h.ts', "d3");
testBlock(20, 'h.ts', "d3");
testBlock(24, 'h.ts', "d3");
goTo.marker("28");
verify.completionListContains("g.ts");
verify.completionListContains("d2");
verify.not.completionListItemsCountIsGreaterThan(2);
function testBlock(offset: number, fileName: string, dir: string) {
for (let m = offset; m < offset + 4; ++m) {
goTo.marker("" + m);
verify.completionListContains(fileName);
verify.completionListContains(dir);
verify.not.completionListItemsCountIsGreaterThan(2);
}
}

View File

@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />
// Exercises whether completions are supplied, conditional on the caret position in the ref comment.
// @Filename: f.ts
//// /*f*/
// @Filename: test.ts
//// /// <reference path/*0*/=/*1*/"/*8*/
//// /// <reference path/*2*/=/*3*/"/*9*/"/*4*/ /*5*///*6*/>/*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);
}

View File

@ -0,0 +1,31 @@
/// <reference path='fourslash.ts' />
// Should give completions for relative references to ts files only when allowJs is false.
// @Filename: test0.ts
//// /// <reference path="/*0*/
//// /// <reference path=".//*1*/
//// /// <reference path="./f/*2*/
// @Filename: f1.ts
//// /*f1*/
// @Filename: f1.js
//// /*f1j*/
// @Filename: f1.d.ts
//// /*f1d*/
// @Filename: f1.tsx
//// /f2*/
// @Filename: f1.js
//// /*f3*/
// @Filename: f1.jsx
//// /*f4*/
// @Filename: f1.cs
//// /*f5*/
for (let i = 0; i < 3; i++) {
goTo.marker("" + i);
verify.completionListContains("f1.ts");
verify.completionListContains("f1.d.ts");
verify.completionListContains("f1.tsx");
verify.not.completionListItemsCountIsGreaterThan(3);
}

View File

@ -1,15 +1,13 @@
/// <reference path='fourslash.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
//// /// <reference path="/*0*/
//// /// <reference path="./*1*/
//// /// <reference path="../*2*/
//// /// <reference path=".//*3*/
//// /// <reference path="./f/*4*/" />
//// /// <reference path=".//*1*/
//// /// <reference path="./f/*2*/
// @Filename: f1.ts
//// /*f1*/
@ -17,17 +15,21 @@
//// /*f1j*/
// @Filename: f1.d.ts
//// /*f1d*/
// @Filename: f2.tsx
// @Filename: f1.tsx
//// /f2*/
// @Filename: f4.jsx
// @Filename: f1.js
//// /*f3*/
// @Filename: f1.jsx
//// /*f4*/
// @Filename: f1.cs
//// /*f5*/
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 3; i++) {
goTo.marker("" + i);
verify.completionListContains("f1.ts");
verify.completionListContains("f1.js");
verify.completionListContains("f1.d.ts");
verify.completionListContains("f2.tsx");
verify.completionListContains("f4.jsx");
verify.completionListContains("f1.tsx");
verify.completionListContains("f1.js");
verify.completionListContains("f1.jsx");
verify.not.completionListItemsCountIsGreaterThan(5);
}

View File

@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />
// Exercises completions for hidden files (ie: those beginning with '.')
// @Filename: f.ts
//// /*f*/
// @Filename: .hidden.ts
//// /*hidden*/
// @Filename: test.ts
//// /// <reference path="/*0*/
//// /// <reference path="./*1*/
//// /// <reference path=".//*2*/
//// /// <reference path=".\/*3*/
for(let m of ["0", "1", "2", "3"]) {
goTo.marker(m);
verify.completionListContains("f.ts");
verify.not.completionListItemsCountIsGreaterThan(1);
}

View File

@ -0,0 +1,34 @@
/// <reference path='fourslash.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
//// /// <reference path="f/*0*/
//// /// <reference path="./f/*1*/
//// /// <reference path="./d/*2*/
//// /// <reference path="f1/*3*/
//// /// <reference path="f1./*4*/
//// /// <reference path="f1.t/*5*/
//// /// <reference path="f1.ts/*6*/
//// /// <reference path="./f1/*7*/
//// /// <reference path="./f1./*8*/
//// /// <reference path="./f1.t/*9*/
//// /// <reference path="./f1.ts/*10*/
for (let m = 0; m < 11; ++m) {
goTo.marker("" + m);
verify.completionListContains("f1.ts");
verify.completionListContains("f2.ts");
verify.completionListContains("d");
verify.not.completionListItemsCountIsGreaterThan(3);
}

View File

@ -0,0 +1,77 @@
/// <reference path='fourslash.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
//// /// <reference path="/*0*/
//// /// <reference path=".//*1*/
//// /// <reference path="./*2*/
//// /// <reference path="../*3*/
//// /// <reference path="d3/*4*/
//// /// <reference path="..//*5*/
//// /// <reference path="..\/*6*/
//// /// <reference path="../..//*7*/
//// /// <reference path="d3//*8*/
//// /// <reference path="./d3//*9*/
//// /// <reference path="d3/d4//*10*/
//// /// <reference path="./d3/d4//*11*/
workingDirCompletions();
parentDirCompletions();
childDirCompletions();
function workingDirCompletions() {
for (let m = 0; m < 5; ++m) {
goTo.marker("" + m);
verify.completionListContains("h.ts");
verify.completionListContains("d3");
verify.not.completionListItemsCountIsGreaterThan(2);
}
}
function parentDirCompletions() {
for (let m of ["5", "6"]) {
goTo.marker(m);
verify.completionListContains("g.ts");
verify.completionListContains("d2");
verify.not.completionListItemsCountIsGreaterThan(2);
}
goTo.marker("7");
verify.completionListContains("f.ts");
verify.completionListContains("d1");
verify.not.completionListItemsCountIsGreaterThan(2);
}
function childDirCompletions() {
for (let m of ["8", "9"]) {
goTo.marker(m);
verify.completionListContains("i.ts");
verify.completionListContains("d4");
verify.not.completionListItemsCountIsGreaterThan(2);
}
for (let m of ["10", "11"]) {
goTo.marker(m);
verify.completionListContains("j.ts");
verify.not.completionListItemsCountIsGreaterThan(1);
}
}