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
Changes from 40 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
@@ -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;
@@ -100,6 +102,7 @@ namespace ts {
getAliasedSymbol: resolveAlias,
getEmitResolver,
getExportsOfModule: getExportsOfModuleAsArray,
getAmbientModules,

getJsxElementAttributesType,
getJsxIntrinsicTagNames,
@@ -20019,5 +20022,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;
}
}
}
15 changes: 6 additions & 9 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
@@ -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;
}
@@ -168,22 +168,19 @@ namespace ts {

const typeReferenceExtensions = [".d.ts"];

function getEffectiveTypeRoots(options: CompilerOptions, host: ModuleResolutionHost) {
export function getEffectiveTypeRoots(options: CompilerOptions, currentDirectory: string) {
if (options.typeRoots) {
return options.typeRoots;
}

let currentDirectory: string;
if (options.configFilePath) {
currentDirectory = getDirectoryPath(options.configFilePath);
}
else if (host.getCurrentDirectory) {
currentDirectory = host.getCurrentDirectory();
}

if (!currentDirectory) {
return undefined;
}

return map(defaultTypeRoots, d => combinePaths(currentDirectory, d));
}

@@ -201,7 +198,7 @@ namespace ts {
traceEnabled
};

const typeRoots = getEffectiveTypeRoots(options, host);
const typeRoots = getEffectiveTypeRoots(options, host.getCurrentDirectory && host.getCurrentDirectory());
if (traceEnabled) {
if (containingFile === undefined) {
if (typeRoots === undefined) {
@@ -1062,7 +1059,7 @@ namespace ts {
// Walk the primary type lookup locations
const result: string[] = [];
if (host.directoryExists && host.getDirectories) {
const typeRoots = getEffectiveTypeRoots(options, host);
const typeRoots = getEffectiveTypeRoots(options, host.getCurrentDirectory && host.getCurrentDirectory());
if (typeRoots) {
for (const root of typeRoots) {
if (host.directoryExists(root)) {
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
@@ -1889,6 +1889,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[];
107 changes: 80 additions & 27 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
@@ -263,22 +263,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;
}
@@ -288,6 +297,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);
@@ -669,10 +687,10 @@ namespace FourSlash {
}
}

public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string) {
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
const completions = this.getCompletionListAtCaret();
if (completions) {
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex);
}
else {
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
@@ -688,25 +706,32 @@ namespace FourSlash {
* @param expectedText the text associated with the symbol
* @param expectedDocumentation the documentation text associated with the symbol
* @param expectedKind the kind of symbol (see ScriptElementKind)
* @param spanIndex the index of the range that the completion item's replacement text span should match
*/
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string) {
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
const that = this;
let replacementSpan: ts.TextSpan;
if (spanIndex !== undefined) {
replacementSpan = this.getTextSpanForRangeAtIndex(spanIndex);
}

function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
const details = that.getCompletionEntryDetails(entry.name);
const documentation = ts.displayPartsToString(details.documentation);
const text = ts.displayPartsToString(details.displayParts);
if (expectedText && expectedDocumentation) {
return (documentation === expectedDocumentation && text === expectedText) ? true : false;

// If any of the expected values are undefined, assume that users don't
// care about them.
if (replacementSpan && !TestState.textSpansEqual(replacementSpan, entry.replacementSpan)) {
return false;
}
else if (expectedText && !expectedDocumentation) {
return text === expectedText ? true : false;
else if (expectedText && text !== expectedText) {
return false;
}
else if (expectedDocumentation && !expectedText) {
return documentation === expectedDocumentation ? true : false;
else if (expectedDocumentation && documentation !== expectedDocumentation) {
return false;
}
// Because expectedText and expectedDocumentation are undefined, we assume that
// users don"t care to compare them so we will treat that entry as if the entry has matching text and documentation
// and keep it in the list of filtered entry.

return true;
}

@@ -730,6 +755,10 @@ namespace FourSlash {
if (expectedKind) {
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + ".";
}
if (replacementSpan) {
const spanText = filterCompletions[0].replacementSpan ? stringify(filterCompletions[0].replacementSpan) : undefined;
error += "Expected replacement span: " + stringify(replacementSpan) + " to equal: " + spanText + ".";
}
this.raiseError(error);
}
}
@@ -2167,7 +2196,7 @@ namespace FourSlash {
return text.substring(startPos, endPos);
}

private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string) {
private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.name === name) {
@@ -2186,6 +2215,11 @@ namespace FourSlash {
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + name));
}

if (spanIndex !== undefined) {
const span = this.getTextSpanForRangeAtIndex(spanIndex);
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
}

return;
}
}
@@ -2242,6 +2276,17 @@ namespace FourSlash {
return `line ${(pos.line + 1)}, col ${pos.character}`;
}

private getTextSpanForRangeAtIndex(index: number): ts.TextSpan {
const ranges = this.getRanges();
if (ranges && ranges.length > index) {
const range = ranges[index];
return { start: range.start, length: range.end - range.start };
}
else {
this.raiseError("Supplied span index: " + index + " does not exist in range list of size: " + (ranges ? 0 : ranges.length));
}
}

public getMarkerByName(markerName: string) {
const markerPos = this.testData.markerPositions[markerName];
if (markerPos === undefined) {
@@ -2265,6 +2310,10 @@ namespace FourSlash {
public resetCancelled(): void {
this.cancellationToken.resetCancelled();
}

private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) {
return a && b && a.start === b.start && a.length === b.length;
}
}

export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -2273,12 +2322,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);
}
@@ -2831,12 +2884,12 @@ namespace FourSlashInterface {

// Verifies the completion list contains the specified symbol. The
// completion list is brought up if necessary
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string) {
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
if (this.negative) {
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind);
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
}
else {
this.state.verifyCompletionListContains(symbol, text, documentation, kind);
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex);
}
}

3 changes: 3 additions & 0 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading