Skip to content

Commit 43e1915

Browse files
authored
Merge pull request #9353 from Microsoft/import_completions_pr
Fix 188: Autocomplete for imports and triple slash reference paths
2 parents a63c1c8 + 548e143 commit 43e1915

File tree

67 files changed

+1800
-130
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1800
-130
lines changed

Diff for: src/compiler/checker.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
/* @internal */
44
namespace ts {
5+
const ambientModuleSymbolRegex = /^".+"$/;
6+
57
let nextSymbolId = 1;
68
let nextNodeId = 1;
79
let nextMergeId = 1;
@@ -100,6 +102,7 @@ namespace ts {
100102
getAliasedSymbol: resolveAlias,
101103
getEmitResolver,
102104
getExportsOfModule: getExportsOfModuleAsArray,
105+
getAmbientModules,
103106

104107
getJsxElementAttributesType,
105108
getJsxIntrinsicTagNames,
@@ -20020,5 +20023,15 @@ namespace ts {
2002020023
return true;
2002120024
}
2002220025
}
20026+
20027+
function getAmbientModules(): Symbol[] {
20028+
const result: Symbol[] = [];
20029+
for (const sym in globals) {
20030+
if (ambientModuleSymbolRegex.test(sym)) {
20031+
result.push(globals[sym]);
20032+
}
20033+
}
20034+
return result;
20035+
}
2002320036
}
2002420037
}

Diff for: src/compiler/program.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ namespace ts {
88

99
const emptyArray: any[] = [];
1010

11-
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean): string {
11+
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string {
1212
while (true) {
13-
const fileName = combinePaths(searchPath, "tsconfig.json");
13+
const fileName = combinePaths(searchPath, configName);
1414
if (fileExists(fileName)) {
1515
return fileName;
1616
}
@@ -166,7 +166,7 @@ namespace ts {
166166

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

169-
function getEffectiveTypeRoots(options: CompilerOptions, host: ModuleResolutionHost): string[] | undefined {
169+
export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined {
170170
if (options.typeRoots) {
171171
return options.typeRoots;
172172
}
@@ -186,7 +186,7 @@ namespace ts {
186186
* Returns the path to every node_modules/@types directory from some ancestor directory.
187187
* Returns undefined if there are none.
188188
*/
189-
function getDefaultTypeRoots(currentDirectory: string, host: ModuleResolutionHost): string[] | undefined {
189+
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
190190
if (!host.directoryExists) {
191191
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
192192
// And if it doesn't exist, tough.

Diff for: src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1889,6 +1889,7 @@ namespace ts {
18891889
getJsxElementAttributesType(elementNode: JsxOpeningLikeElement): Type;
18901890
getJsxIntrinsicTagNames(): Symbol[];
18911891
isOptionalParameter(node: ParameterDeclaration): boolean;
1892+
getAmbientModules(): Symbol[];
18921893

18931894
// Should not be called directly. Should only be accessed through the Program instance.
18941895
/* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];

Diff for: src/harness/fourslash.ts

+80-27
Original file line numberDiff line numberDiff line change
@@ -263,22 +263,31 @@ namespace FourSlash {
263263
constructor(private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) {
264264
// Create a new Services Adapter
265265
this.cancellationToken = new TestCancellationToken();
266-
const compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
267-
if (compilationOptions.typeRoots) {
268-
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
269-
}
266+
let compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
270267
compilationOptions.skipDefaultLibCheck = true;
271268

272-
const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
273-
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
274-
this.languageService = languageServiceAdapter.getLanguageService();
275-
276269
// Initialize the language service with all the scripts
277270
let startResolveFileRef: FourSlashFile;
278271

279272
ts.forEach(testData.files, file => {
280273
// Create map between fileName and its content for easily looking up when resolveReference flag is specified
281274
this.inputFiles[file.fileName] = file.content;
275+
276+
if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") {
277+
const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
278+
assert.isTrue(configJson.config !== undefined);
279+
280+
// Extend our existing compiler options so that we can also support tsconfig only options
281+
if (configJson.config.compilerOptions) {
282+
const baseDirectory = ts.normalizePath(ts.getDirectoryPath(file.fileName));
283+
const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);
284+
285+
if (!tsConfig.errors || !tsConfig.errors.length) {
286+
compilationOptions = ts.extend(compilationOptions, tsConfig.options);
287+
}
288+
}
289+
}
290+
282291
if (!startResolveFileRef && file.fileOptions[metadataOptionNames.resolveReference] === "true") {
283292
startResolveFileRef = file;
284293
}
@@ -288,6 +297,15 @@ namespace FourSlash {
288297
}
289298
});
290299

300+
301+
if (compilationOptions.typeRoots) {
302+
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
303+
}
304+
305+
const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
306+
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
307+
this.languageService = languageServiceAdapter.getLanguageService();
308+
291309
if (startResolveFileRef) {
292310
// Add the entry-point file itself into the languageServiceShimHost
293311
this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true);
@@ -730,10 +748,10 @@ namespace FourSlash {
730748
}
731749
}
732750

733-
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string) {
751+
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
734752
const completions = this.getCompletionListAtCaret();
735753
if (completions) {
736-
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
754+
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex);
737755
}
738756
else {
739757
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
@@ -749,25 +767,32 @@ namespace FourSlash {
749767
* @param expectedText the text associated with the symbol
750768
* @param expectedDocumentation the documentation text associated with the symbol
751769
* @param expectedKind the kind of symbol (see ScriptElementKind)
770+
* @param spanIndex the index of the range that the completion item's replacement text span should match
752771
*/
753-
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string) {
772+
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
754773
const that = this;
774+
let replacementSpan: ts.TextSpan;
775+
if (spanIndex !== undefined) {
776+
replacementSpan = this.getTextSpanForRangeAtIndex(spanIndex);
777+
}
778+
755779
function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
756780
const details = that.getCompletionEntryDetails(entry.name);
757781
const documentation = ts.displayPartsToString(details.documentation);
758782
const text = ts.displayPartsToString(details.displayParts);
759-
if (expectedText && expectedDocumentation) {
760-
return (documentation === expectedDocumentation && text === expectedText) ? true : false;
783+
784+
// If any of the expected values are undefined, assume that users don't
785+
// care about them.
786+
if (replacementSpan && !TestState.textSpansEqual(replacementSpan, entry.replacementSpan)) {
787+
return false;
761788
}
762-
else if (expectedText && !expectedDocumentation) {
763-
return text === expectedText ? true : false;
789+
else if (expectedText && text !== expectedText) {
790+
return false;
764791
}
765-
else if (expectedDocumentation && !expectedText) {
766-
return documentation === expectedDocumentation ? true : false;
792+
else if (expectedDocumentation && documentation !== expectedDocumentation) {
793+
return false;
767794
}
768-
// Because expectedText and expectedDocumentation are undefined, we assume that
769-
// users don"t care to compare them so we will treat that entry as if the entry has matching text and documentation
770-
// and keep it in the list of filtered entry.
795+
771796
return true;
772797
}
773798

@@ -791,6 +816,10 @@ namespace FourSlash {
791816
if (expectedKind) {
792817
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + ".";
793818
}
819+
if (replacementSpan) {
820+
const spanText = filterCompletions[0].replacementSpan ? stringify(filterCompletions[0].replacementSpan) : undefined;
821+
error += "Expected replacement span: " + stringify(replacementSpan) + " to equal: " + spanText + ".";
822+
}
794823
this.raiseError(error);
795824
}
796825
}
@@ -2188,7 +2217,7 @@ namespace FourSlash {
21882217
return text.substring(startPos, endPos);
21892218
}
21902219

2191-
private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string) {
2220+
private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
21922221
for (let i = 0; i < items.length; i++) {
21932222
const item = items[i];
21942223
if (item.name === name) {
@@ -2207,6 +2236,11 @@ namespace FourSlash {
22072236
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + name));
22082237
}
22092238

2239+
if (spanIndex !== undefined) {
2240+
const span = this.getTextSpanForRangeAtIndex(spanIndex);
2241+
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
2242+
}
2243+
22102244
return;
22112245
}
22122246
}
@@ -2263,6 +2297,17 @@ namespace FourSlash {
22632297
return `line ${(pos.line + 1)}, col ${pos.character}`;
22642298
}
22652299

2300+
private getTextSpanForRangeAtIndex(index: number): ts.TextSpan {
2301+
const ranges = this.getRanges();
2302+
if (ranges && ranges.length > index) {
2303+
const range = ranges[index];
2304+
return { start: range.start, length: range.end - range.start };
2305+
}
2306+
else {
2307+
this.raiseError("Supplied span index: " + index + " does not exist in range list of size: " + (ranges ? 0 : ranges.length));
2308+
}
2309+
}
2310+
22662311
public getMarkerByName(markerName: string) {
22672312
const markerPos = this.testData.markerPositions[markerName];
22682313
if (markerPos === undefined) {
@@ -2286,6 +2331,10 @@ namespace FourSlash {
22862331
public resetCancelled(): void {
22872332
this.cancellationToken.resetCancelled();
22882333
}
2334+
2335+
private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) {
2336+
return a && b && a.start === b.start && a.length === b.length;
2337+
}
22892338
}
22902339

22912340
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -2294,12 +2343,16 @@ namespace FourSlash {
22942343
}
22952344

22962345
export function runFourSlashTestContent(basePath: string, testType: FourSlashTestType, content: string, fileName: string): void {
2346+
// Give file paths an absolute path for the virtual file system
2347+
const absoluteBasePath = ts.combinePaths(Harness.virtualFileSystemRoot, basePath);
2348+
const absoluteFileName = ts.combinePaths(Harness.virtualFileSystemRoot, fileName);
2349+
22972350
// Parse out the files and their metadata
2298-
const testData = parseTestData(basePath, content, fileName);
2299-
const state = new TestState(basePath, testType, testData);
2351+
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
2352+
const state = new TestState(absoluteBasePath, testType, testData);
23002353
const output = ts.transpileModule(content, { reportDiagnostics: true });
23012354
if (output.diagnostics.length > 0) {
2302-
throw new Error(`Syntax error in ${basePath}: ${output.diagnostics[0].messageText}`);
2355+
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics[0].messageText}`);
23032356
}
23042357
runCode(output.outputText, state);
23052358
}
@@ -2852,12 +2905,12 @@ namespace FourSlashInterface {
28522905

28532906
// Verifies the completion list contains the specified symbol. The
28542907
// completion list is brought up if necessary
2855-
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string) {
2908+
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
28562909
if (this.negative) {
2857-
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind);
2910+
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
28582911
}
28592912
else {
2860-
this.state.verifyCompletionListContains(symbol, text, documentation, kind);
2913+
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex);
28612914
}
28622915
}
28632916

Diff for: src/harness/harness.ts

+3
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,9 @@ namespace Harness {
458458
// harness always uses one kind of new line
459459
const harnessNewLine = "\r\n";
460460

461+
// Root for file paths that are stored in a virtual file system
462+
export const virtualFileSystemRoot = "/";
463+
461464
namespace IOImpl {
462465
declare class Enumerator {
463466
public atEnd(): boolean;

Diff for: src/harness/harnessLanguageService.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ namespace Harness.LanguageService {
123123
}
124124

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

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

136136
public getFilenames(): string[] {
137137
const fileNames: string[] = [];
138-
ts.forEachProperty(this.fileNameToScript, (scriptInfo) => {
138+
for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()){
139+
const scriptInfo = virtualEntry.content;
139140
if (scriptInfo.isRootFile) {
140141
// only include root files here
141142
// 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.
142143
fileNames.push(scriptInfo.fileName);
143144
}
144-
});
145+
}
145146
return fileNames;
146147
}
147148

148149
public getScriptInfo(fileName: string): ScriptInfo {
149-
return this.fileNameToScript[fileName];
150+
const fileEntry = this.virtualFileSystem.traversePath(fileName);
151+
return fileEntry && fileEntry.isFile() ? (<Utils.VirtualFile>fileEntry).content : undefined;
150152
}
151153

152154
public addScript(fileName: string, content: string, isRootFile: boolean): void {
153-
this.fileNameToScript[fileName] = new ScriptInfo(fileName, content, isRootFile);
155+
this.virtualFileSystem.addFile(fileName, new ScriptInfo(fileName, content, isRootFile));
154156
}
155157

156158
public editScript(fileName: string, start: number, end: number, newText: string) {
@@ -171,7 +173,7 @@ namespace Harness.LanguageService {
171173
* @param col 0 based index
172174
*/
173175
public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter {
174-
const script: ScriptInfo = this.fileNameToScript[fileName];
176+
const script: ScriptInfo = this.getScriptInfo(fileName);
175177
assert.isOk(script);
176178

177179
return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position);
@@ -182,8 +184,14 @@ namespace Harness.LanguageService {
182184
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost {
183185
getCompilationSettings() { return this.settings; }
184186
getCancellationToken() { return this.cancellationToken; }
185-
getDirectories(path: string): string[] { return []; }
186-
getCurrentDirectory(): string { return ""; }
187+
getDirectories(path: string): string[] {
188+
const dir = this.virtualFileSystem.traversePath(path);
189+
if (dir && dir.isDirectory()) {
190+
return ts.map((<Utils.VirtualDirectory>dir).getDirectories(), (d) => ts.combinePaths(path, d.name));
191+
}
192+
return [];
193+
}
194+
getCurrentDirectory(): string { return virtualFileSystemRoot; }
187195
getDefaultLibFileName(): string { return Harness.Compiler.defaultLibFileName; }
188196
getScriptFileNames(): string[] { return this.getFilenames(); }
189197
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
@@ -196,6 +204,22 @@ namespace Harness.LanguageService {
196204
return script ? script.version.toString() : undefined;
197205
}
198206

207+
fileExists(fileName: string): boolean {
208+
const script = this.getScriptSnapshot(fileName);
209+
return script !== undefined;
210+
}
211+
readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] {
212+
return ts.matchFiles(path, extensions, exclude, include,
213+
/*useCaseSensitiveFileNames*/false,
214+
this.getCurrentDirectory(),
215+
(p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p));
216+
}
217+
readFile(path: string, encoding?: string): string {
218+
const snapshot = this.getScriptSnapshot(path);
219+
return snapshot.getText(0, snapshot.getLength());
220+
}
221+
222+
199223
log(s: string): void { }
200224
trace(s: string): void { }
201225
error(s: string): void { }

0 commit comments

Comments
 (0)