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
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[];
}