Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 188: Autocomplete for imports and triple slash reference paths #9353

Merged
merged 44 commits into from
Sep 6, 2016
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c06b02a
Adding completions for import and reference directives
riknoll Jun 24, 2016
ccf27d1
Minor fix
riknoll Jun 24, 2016
dbdd989
Import completions for require calls
riknoll Jun 27, 2016
801b493
PR feedback
riknoll Jun 30, 2016
f644da7
Merge branch 'master' into import_completions_pr
riknoll Jul 5, 2016
5c24b35
Refactoring node_modules enumeration code
riknoll Jul 6, 2016
ffc165e
Fixing behavior of resolvePath
riknoll Jul 6, 2016
5c87c5a
Removing forEach reference
riknoll Jul 6, 2016
84a10e4
Some PR feedback
riknoll Jul 25, 2016
ed2da32
Handling more compiler options and minor refactor
riknoll Jul 26, 2016
0b16180
Import completions with rootdirs compiler option
riknoll Jul 27, 2016
dbf19f1
Adding import completions for typings
riknoll Jul 28, 2016
fdbc23e
Add completions for types triple slash directives
riknoll Jul 28, 2016
4ca7e95
Merge remote-tracking branch 'origin/master' into import_completions_…
riknoll Jul 28, 2016
9e797b4
Use getDirectories and condition node modules resolution on moduleRes…
riknoll Jul 28, 2016
4ec8b2b
Refactoring import completions into their own api
riknoll Aug 1, 2016
98a162b
Replacement spans for import completions
riknoll Aug 1, 2016
35cd480
Fixing import completion spans to only include the end of the directo…
riknoll Aug 2, 2016
a5d73bf
No more filtering results
riknoll Aug 2, 2016
8b5a3d9
Refactoring API to remove duplicate spans
riknoll Aug 3, 2016
293ca60
Renamed span to textSpan to better follow other language service APIs
riknoll Aug 3, 2016
ca28823
Fixing shim and normalizing paths
riknoll Aug 4, 2016
0f22079
Remove trailing slashes, remove mostly useless IO, fix script element…
riknoll Aug 5, 2016
ecdbdb3
Fixing the filtering of nested module completions
riknoll Aug 6, 2016
e11d5e9
Cleaning up test cases and adding a few more
riknoll Aug 6, 2016
8a976f1
Moving some utility functions around
riknoll Aug 8, 2016
cc35bd5
Merge remote-tracking branch 'origin/master' into import_completions_pr
riknoll Aug 15, 2016
2f4a855
Use rooted paths in the fourslash virtual file system
riknoll Aug 16, 2016
310bce4
Removing resolvePath from language service host
riknoll Aug 16, 2016
cf7feb3
Responding to PR feedback
riknoll Aug 19, 2016
473be82
Merge remote-tracking branch 'origin/master' into import_completions_pr
riknoll Aug 19, 2016
00facc2
Removing hasProperty check
riknoll Aug 20, 2016
0ebd196
Fixing regex for triple slash references
riknoll Aug 20, 2016
c71c5a8
Using for..of instead of forEach
riknoll Aug 23, 2016
34847f0
Making language service host changes optional
riknoll Aug 25, 2016
276b56d
More PR feedback
riknoll Aug 26, 2016
fb6ff42
Reuse effective type roots code in language service
riknoll Aug 27, 2016
b9b79af
Recombining import completions and regular completion APIs
riknoll Sep 1, 2016
7261866
Cleaning up the completion code and tests
riknoll Sep 1, 2016
c742d16
Merge remote-tracking branch 'origin/master' into import_completions_pr
riknoll Sep 1, 2016
8728b98
Adding comment and removing unnecessary object creation
riknoll Sep 2, 2016
a26d310
Merge remote-tracking branch 'origin/master' into import_completions_pr
riknoll Sep 6, 2016
8f0c7ef
Pass the right host to getEffectiveTyperoots
riknoll Sep 6, 2016
548e143
Merge remote-tracking branch 'origin/master' into import_completions_pr
riknoll Sep 6, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

/* @internal */
namespace ts {
const ambientModuleSymbolRegex = /^".+"$/;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was copying the behavior where we access the modules elsewhere in the checker (see resolveExternalModuleNameWorker())


let nextSymbolId = 1;
let nextNodeId = 1;
let nextMergeId = 1;
Expand Down Expand Up @@ -100,6 +102,7 @@ namespace ts {
getAliasedSymbol: resolveAlias,
getEmitResolver,
getExportsOfModule: getExportsOfModuleAsArray,
getAmbientModules,

getJsxElementAttributesType,
getJsxIntrinsicTagNames,
Expand Down Expand Up @@ -19951,5 +19954,15 @@ namespace ts {
return true;
}
}

function getAmbientModules(): Symbol[] {
const result: Symbol[] = [];
for (const sym in globals) {
if (ambientModuleSymbolRegex.test(sym)) {
result.push(globals[sym]);
}
}
return result;
}
}
}
4 changes: 2 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,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;
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,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[];
Expand Down
135 changes: 124 additions & 11 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,22 +245,31 @@ namespace FourSlash {
constructor(private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) {
// Create a new Services Adapter
this.cancellationToken = new TestCancellationToken();
const compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
if (compilationOptions.typeRoots) {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}
let compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
compilationOptions.skipDefaultLibCheck = true;

const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
this.languageService = languageServiceAdapter.getLanguageService();

// Initialize the language service with all the scripts
let startResolveFileRef: FourSlashFile;

ts.forEach(testData.files, file => {
// Create map between fileName and its content for easily looking up when resolveReference flag is specified
this.inputFiles[file.fileName] = file.content;

if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") {
const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
assert.isTrue(configJson.config !== undefined);

// Extend our existing compiler options so that we can also support tsconfig only options
if (configJson.config.compilerOptions) {
const baseDirectory = ts.normalizePath(ts.getDirectoryPath(file.fileName));
const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);

if (!tsConfig.errors || !tsConfig.errors.length) {
compilationOptions = ts.extend(compilationOptions, tsConfig.options);
}
}
}

if (!startResolveFileRef && file.fileOptions[metadataOptionNames.resolveReference] === "true") {
startResolveFileRef = file;
}
Expand All @@ -270,6 +279,15 @@ namespace FourSlash {
}
});


if (compilationOptions.typeRoots) {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}

const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
this.languageService = languageServiceAdapter.getLanguageService();

if (startResolveFileRef) {
// Add the entry-point file itself into the languageServiceShimHost
this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true);
Expand Down Expand Up @@ -580,6 +598,38 @@ namespace FourSlash {
}
}

public verifyImportModuleCompletionListItemsCountIsGreaterThan(count: number, negative: boolean) {
const completions = this.getImportModuleCompletionListAtCaret();
const itemsCount = completions.entries.length;

if (negative) {
if (itemsCount > count) {
this.raiseError(`Expected import module completion list items count to not be greater than ${count}, but is actually ${itemsCount}`);
}
}
else {
if (itemsCount <= count) {
this.raiseError(`Expected import module completion list items count to be greater than ${count}, but is actually ${itemsCount}`);
}
}
}

public verifyImportModuleCompletionListIsEmpty(negative: boolean) {
const completions = this.getImportModuleCompletionListAtCaret();
if ((!completions || completions.entries.length === 0) && negative) {
this.raiseError("Completion list is empty at caret at position " + this.activeFile.fileName + " " + this.currentCaretPosition);
}
else if (completions && completions.entries.length !== 0 && !negative) {
let errorMsg = "\n" + "Completion List contains: [" + completions.entries[0].name;
for (let i = 1; i < completions.entries.length; i++) {
errorMsg += ", " + completions.entries[i].name;
}
errorMsg += "]\n";

this.raiseError("Completion list is not empty at caret at position " + this.activeFile.fileName + " " + this.currentCaretPosition + errorMsg);
}
}

public verifyCompletionListStartsWithItemsInOrder(items: string[]): void {
if (items.length === 0) {
return;
Expand Down Expand Up @@ -717,6 +767,44 @@ namespace FourSlash {
}
}

public verifyImportModuleCompletionListContains(symbol: string, rangeIndex?: number) {
const completions = this.getImportModuleCompletionListAtCaret();
if (completions) {
const completion = ts.forEach(completions.entries, completion => completion.name === symbol ? completion : undefined);
if (!completion) {
const itemsString = completions.entries.map(item => item.name).join(",\n");
this.raiseError(`Expected "${symbol}" to be in list [${itemsString}]`);
}
else if (rangeIndex !== undefined) {
const ranges = this.getRanges();
if (ranges && ranges.length > rangeIndex) {
const range = ranges[rangeIndex];

const start = completions.textSpan.start;
const end = start + completions.textSpan.length;
if (range.start !== start || range.end !== end) {
this.raiseError(`Expected completion span for '${symbol}', ${stringify(completions.textSpan)}, to cover range ${stringify(range)}`);
}
}
else {
this.raiseError(`Expected completion span for '${symbol}' to cover range at index ${rangeIndex}, but no range was found at that index`);
}
}
}
else {
this.raiseError(`No import module completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
}
}

public verifyImportModuleCompletionListDoesNotContain(symbol: string) {
const completions = this.getImportModuleCompletionListAtCaret();
if (completions) {
if (ts.forEach(completions.entries, completion => completion.name === symbol)) {
this.raiseError(`Import module completion list did contain ${symbol}`);
}
}
}

public verifyCompletionEntryDetails(entryName: string, expectedText: string, expectedDocumentation?: string, kind?: string) {
const details = this.getCompletionEntryDetails(entryName);

Expand Down Expand Up @@ -803,6 +891,10 @@ namespace FourSlash {
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition);
}

private getImportModuleCompletionListAtCaret() {
return this.languageService.getImportModuleCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition);
}

private getCompletionEntryDetails(entryName: string) {
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName);
}
Expand Down Expand Up @@ -2255,12 +2347,16 @@ namespace FourSlash {
}

export function runFourSlashTestContent(basePath: string, testType: FourSlashTestType, content: string, fileName: string): void {
// Give file paths an absolute path for the virtual file system
const absoluteBasePath = ts.combinePaths(Harness.virtualFileSystemRoot, basePath);
const absoluteFileName = ts.combinePaths(Harness.virtualFileSystemRoot, fileName);

// Parse out the files and their metadata
const testData = parseTestData(basePath, content, fileName);
const state = new TestState(basePath, testType, testData);
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
const state = new TestState(absoluteBasePath, testType, testData);
const output = ts.transpileModule(content, { reportDiagnostics: true });
if (output.diagnostics.length > 0) {
throw new Error(`Syntax error in ${basePath}: ${output.diagnostics[0].messageText}`);
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics[0].messageText}`);
}
runCode(output.outputText, state);
}
Expand Down Expand Up @@ -2828,6 +2924,23 @@ namespace FourSlashInterface {
this.state.verifyCompletionListItemsCountIsGreaterThan(count, this.negative);
}

public importModuleCompletionListContains(symbol: string, rangeIndex?: number): void {
if (this.negative) {
this.state.verifyImportModuleCompletionListDoesNotContain(symbol);
}
else {
this.state.verifyImportModuleCompletionListContains(symbol, rangeIndex);
}
}

public importModuleCompletionListItemsCountIsGreaterThan(count: number): void {
this.state.verifyImportModuleCompletionListItemsCountIsGreaterThan(count, this.negative);
}

public importModuleCompletionListIsEmpty(): void {
this.state.verifyImportModuleCompletionListIsEmpty(this.negative);
}

public assertHasRanges(ranges: FourSlash.Range[]) {
assert(ranges.length !== 0, "Array of ranges is expected to be non-empty");
}
Expand Down
3 changes: 3 additions & 0 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ namespace Harness {
// harness always uses one kind of new line
const harnessNewLine = "\r\n";

// Root for file paths that are stored in a virtual file system
export const virtualFileSystemRoot = "/";

namespace IOImpl {
declare class Enumerator {
public atEnd(): boolean;
Expand Down
43 changes: 35 additions & 8 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ namespace Harness.LanguageService {
}

export class LanguageServiceAdapterHost {
protected fileNameToScript = ts.createMap<ScriptInfo>();
protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false);

constructor(protected cancellationToken = DefaultHostCancellationToken.Instance,
protected settings = ts.getDefaultCompilerOptions()) {
Expand All @@ -135,22 +135,24 @@ namespace Harness.LanguageService {

public getFilenames(): string[] {
const fileNames: string[] = [];
ts.forEachProperty(this.fileNameToScript, (scriptInfo) => {
for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()){
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.
fileNames.push(scriptInfo.fileName);
}
});
}
return fileNames;
}

public getScriptInfo(fileName: string): ScriptInfo {
return this.fileNameToScript[fileName];
const fileEntry = this.virtualFileSystem.traversePath(fileName);
return fileEntry && fileEntry.isFile() ? (<Utils.VirtualFile>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) {
Expand All @@ -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);
Expand All @@ -182,8 +184,14 @@ namespace Harness.LanguageService {
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost {
getCompilationSettings() { return this.settings; }
getCancellationToken() { return this.cancellationToken; }
getDirectories(path: string): string[] { return []; }
getCurrentDirectory(): string { return ""; }
getDirectories(path: string): string[] {
const dir = this.virtualFileSystem.traversePath(path);
if (dir && dir.isDirectory()) {
return ts.map((<Utils.VirtualDirectory>dir).getDirectories(), (d) => ts.combinePaths(path, d.name));
}
return [];
}
getCurrentDirectory(): string { return virtualFileSystemRoot; }
getDefaultLibFileName(): string { return Harness.Compiler.defaultLibFileName; }
getScriptFileNames(): string[] { return this.getFilenames(); }
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
Expand All @@ -196,6 +204,22 @@ 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,
this.getCurrentDirectory(),
(p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p));
}
readFile(path: string, encoding?: string): string {
const snapshot = this.getScriptSnapshot(path);
return snapshot.getText(0, snapshot.getLength());
}


log(s: string): void { }
trace(s: string): void { }
error(s: string): void { }
Expand Down Expand Up @@ -381,6 +405,9 @@ namespace Harness.LanguageService {
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position));
}
getImportModuleCompletionsAtPosition(fileName: string, position: number): ts.ImportCompletionInfo {
return unwrapJSONCallResult(this.shim.getImportModuleCompletionsAtPosition(fileName, position));
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName));
}
Expand Down
Loading