Skip to content

Commit 2b566b9

Browse files
author
Andy
authored
Add exported members of all project files in the global completion list (#19069)
* checker.ts: Remove null check on symbols * tsserverProjectSystem.ts: add two tests * client.ts, completions.ts, types.ts: Add codeActions member to CompletionEntryDetails * protocol.ts, session.ts: Add codeActions member to CompletionEntryDetails protocol * protocol.ts, session.ts, types.ts: add hasAction to CompletionEntry * session.ts, services.ts, types.ts: Add formattingOptions parameter to getCompletionEntryDetails * completions.ts: define SymbolOriginInfo type * completions.ts, services.ts: Add allSourceFiles parameter to getCompletionsAtPosition * completions.ts, services.ts: Plumb allSourceFiles into new function getSymbolsFromOtherSourceFileExports inside getCompletionData * completions.ts: add symbolToOriginInfoMap parameter to getCompletionEntriesFromSymbols and to return value of getCompletionData * utilities.ts: Add getOtherModuleSymbols, getUniqueSymbolIdAsString, getUniqueSymbolId * completions.ts: Set CompletionEntry.hasAction when symbol is found in symbolToOriginInfoMap (meaning there's an import action) * completions.ts: Populate list with possible exports (implement getSymbolsFromOtherSourceFileExports) * completions.ts, services.ts: Plumb host and rulesProvider into getCompletionEntryDetails * completions.ts: Add TODO comment * importFixes.ts: Add types ImportDeclarationMap and ImportCodeFixContext * Move getImportDeclarations into getCodeActionForImport, immediately after the implementation * importFixes.ts: Move createChangeTracker into getCodeActionForImport, immediately after getImportDeclarations * importFixes.ts: Add convertToImportCodeFixContext function and reference it from the getCodeActions lambda * importFixes.ts: Add context: ImportCodeFixContext parameter to getCodeActionForImport, update call sites, destructure it, use compilerOptions in getModuleSpecifierForNewImport * importFixes.ts: Remove moduleSymbol parameter from getImportDeclarations and use the ambient one * importFixes.ts: Use cachedImportDeclarations from context in getCodeActionForImport * importFixes.ts: Move createCodeAction out, immediately above convertToImportCodeFixContext * Move the declaration for lastImportDeclaration out of the getCodeActions lambda into getCodeActionForImport * importFixes.ts: Use symbolToken in getCodeActionForImport * importFixes.ts: Remove useCaseSensitiveFileNames altogether from getCodeActions lambda * importFixes.ts: Remove local getUniqueSymbolId function and add checker parameter to calls to it * importFixes.ts: Move getCodeActionForImport out into an export, immediately below convertToImportCodeFixContext * completions.ts: In getCompletionEntryDetails, if there's symbolOriginInfo, call getCodeActionForImport * importFixes.ts: Create and use importFixContext within getCodeActions lambda * importFixes.ts: Use local newLineCharacter instead of context.newLineCharacter in getCodeActionForImport * importFixes.ts: Use local host instead of context.host in getCodeActionForImport * importFixes.ts: Remove dummy getCanonicalFileName line * Filter symbols after gathering exports instead of before * Lint * Test, fix bugs, refactor * Suggestions from code review * Update api baseline * Fix bug if previousToken is not an Identifier * Replace `startsWith` with `stringContainsCharactersInOrder`
1 parent 3a84b66 commit 2b566b9

36 files changed

+1104
-582
lines changed

src/compiler/checker.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ namespace ts {
314314
const jsObjectLiteralIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false);
315315

316316
const globals = createSymbolTable();
317+
let ambientModulesCache: Symbol[] | undefined;
317318
/**
318319
* List of every ambient module with a "*" wildcard.
319320
* Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches.
@@ -25586,13 +25587,16 @@ namespace ts {
2558625587
}
2558725588

2558825589
function getAmbientModules(): Symbol[] {
25589-
const result: Symbol[] = [];
25590-
globals.forEach((global, sym) => {
25591-
if (ambientModuleSymbolRegex.test(unescapeLeadingUnderscores(sym))) {
25592-
result.push(global);
25593-
}
25594-
});
25595-
return result;
25590+
if (!ambientModulesCache) {
25591+
ambientModulesCache = [];
25592+
globals.forEach((global, sym) => {
25593+
// No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module.
25594+
if (ambientModuleSymbolRegex.test(sym as string)) {
25595+
ambientModulesCache.push(global);
25596+
}
25597+
});
25598+
}
25599+
return ambientModulesCache;
2559625600
}
2559725601

2559825602
function checkGrammarImportCallExpression(node: ImportCall): boolean {

src/compiler/commandLineParser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,8 +1183,8 @@ namespace ts {
11831183
}
11841184
}
11851185

1186-
function isDoubleQuotedString(node: Node) {
1187-
return node.kind === SyntaxKind.StringLiteral && getSourceTextOfNodeFromSourceFile(sourceFile, node).charCodeAt(0) === CharacterCodes.doubleQuote;
1186+
function isDoubleQuotedString(node: Node): boolean {
1187+
return isStringLiteral(node) && isStringDoubleQuoted(node, sourceFile);
11881188
}
11891189
}
11901190

src/compiler/core.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,18 @@ namespace ts {
191191
}
192192
return undefined;
193193
}
194+
195+
/** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */
196+
export function firstDefined<T, U>(array: ReadonlyArray<T> | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
197+
for (let i = 0; i < array.length; i++) {
198+
const result = callback(array[i], i);
199+
if (result !== undefined) {
200+
return result;
201+
}
202+
}
203+
return undefined;
204+
}
205+
194206
/**
195207
* Iterates through the parent chain of a node and performs the callback on each parent until the callback
196208
* returns a truthy value, then returns that value.
@@ -261,6 +273,16 @@ namespace ts {
261273
return undefined;
262274
}
263275

276+
export function findLast<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): T | undefined {
277+
for (let i = array.length - 1; i >= 0; i--) {
278+
const value = array[i];
279+
if (predicate(value, i)) {
280+
return value;
281+
}
282+
}
283+
return undefined;
284+
}
285+
264286
/** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */
265287
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): number {
266288
for (let i = 0; i < array.length; i++) {
@@ -1147,6 +1169,14 @@ namespace ts {
11471169
return result;
11481170
}
11491171

1172+
export function arrayToNumericMap<T>(array: ReadonlyArray<T>, makeKey: (value: T) => number): T[] {
1173+
const result: T[] = [];
1174+
for (const value of array) {
1175+
result[makeKey(value)] = value;
1176+
}
1177+
return result;
1178+
}
1179+
11501180
/**
11511181
* Creates a set from the elements of an array.
11521182
*

src/compiler/diagnosticMessages.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3661,15 +3661,15 @@
36613661
"category": "Error",
36623662
"code": 90010
36633663
},
3664-
"Import {0} from {1}.": {
3664+
"Import '{0}' from \"{1}\".": {
36653665
"category": "Message",
36663666
"code": 90013
36673667
},
36683668
"Change '{0}' to '{1}'.": {
36693669
"category": "Message",
36703670
"code": 90014
36713671
},
3672-
"Add {0} to existing import declaration from {1}.": {
3672+
"Add '{0}' to existing import declaration from \"{1}\".": {
36733673
"category": "Message",
36743674
"code": 90015
36753675
},

src/compiler/moduleNameResolver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ namespace ts {
128128
}
129129
}
130130

131-
export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined {
131+
export interface GetEffectiveTypeRootsHost {
132+
directoryExists?(directoryName: string): boolean;
133+
getCurrentDirectory?(): string;
134+
}
135+
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
132136
if (options.typeRoots) {
133137
return options.typeRoots;
134138
}

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ namespace ts {
10551055
export interface StringLiteral extends LiteralExpression {
10561056
kind: SyntaxKind.StringLiteral;
10571057
/* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms).
1058+
/** Note: this is only set when synthesizing a node, not during parsing. */
10581059
/* @internal */ singleQuote?: boolean;
10591060
}
10601061

src/compiler/utilities.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,17 @@ namespace ts {
520520
}
521521
}
522522

523+
/* @internal */
524+
export function isAnyImportSyntax(node: Node): node is AnyImportSyntax {
525+
switch (node.kind) {
526+
case SyntaxKind.ImportDeclaration:
527+
case SyntaxKind.ImportEqualsDeclaration:
528+
return true;
529+
default:
530+
return false;
531+
}
532+
}
533+
523534
// Gets the nearest enclosing block scope container that has the provided node
524535
// as a descendant, that is not the provided node.
525536
export function getEnclosingBlockScopeContainer(node: Node): Node {
@@ -1375,6 +1386,10 @@ namespace ts {
13751386
return charCode === CharacterCodes.singleQuote || charCode === CharacterCodes.doubleQuote;
13761387
}
13771388

1389+
export function isStringDoubleQuoted(string: StringLiteral, sourceFile: SourceFile): boolean {
1390+
return getSourceTextOfNodeFromSourceFile(sourceFile, string).charCodeAt(0) === CharacterCodes.doubleQuote;
1391+
}
1392+
13781393
/**
13791394
* Returns true if the node is a variable declaration whose initializer is a function expression.
13801395
* This function does not test if the node is in a JavaScript file or not.

src/harness/fourslash.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -783,10 +783,10 @@ namespace FourSlash {
783783
});
784784
}
785785

786-
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
786+
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
787787
const completions = this.getCompletionListAtCaret();
788788
if (completions) {
789-
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex);
789+
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex, hasAction);
790790
}
791791
else {
792792
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
@@ -1127,7 +1127,7 @@ Actual: ${stringify(fullActual)}`);
11271127
}
11281128

11291129
private getCompletionEntryDetails(entryName: string) {
1130-
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName);
1130+
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings);
11311131
}
11321132

11331133
private getReferencesAtCaret() {
@@ -2289,6 +2289,29 @@ Actual: ${stringify(fullActual)}`);
22892289
this.applyCodeActions(this.getCodeFixActions(fileName, errorCode), index);
22902290
}
22912291

2292+
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
2293+
this.goToMarker(markerName);
2294+
2295+
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name);
2296+
2297+
if (!actualCompletion.hasAction) {
2298+
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
2299+
}
2300+
2301+
const details = this.getCompletionEntryDetails(options.name);
2302+
if (details.codeActions.length !== 1) {
2303+
this.raiseError(`Expected one code action, got ${details.codeActions.length}`);
2304+
}
2305+
2306+
if (details.codeActions[0].description !== options.description) {
2307+
this.raiseError(`Expected description to be:\n${options.description}\ngot:\n${details.codeActions[0].description}`);
2308+
}
2309+
2310+
this.applyCodeActions(details.codeActions);
2311+
2312+
this.verifyNewContent(options);
2313+
}
2314+
22922315
public verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) {
22932316
const ranges = this.getRanges();
22942317
if (ranges.length !== 1) {
@@ -2360,6 +2383,10 @@ Actual: ${stringify(fullActual)}`);
23602383
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
23612384
}
23622385

2386+
this.verifyNewContent(options);
2387+
}
2388+
2389+
private verifyNewContent(options: FourSlashInterface.NewContentOptions) {
23632390
if (options.newFileContent) {
23642391
assert(!options.newRangeContent);
23652392
this.verifyCurrentFileContent(options.newFileContent);
@@ -2933,7 +2960,15 @@ Actual: ${stringify(fullActual)}`);
29332960
return text.substring(startPos, endPos);
29342961
}
29352962

2936-
private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
2963+
private assertItemInCompletionList(
2964+
items: ts.CompletionEntry[],
2965+
name: string,
2966+
text: string | undefined,
2967+
documentation: string | undefined,
2968+
kind: string | undefined,
2969+
spanIndex: number | undefined,
2970+
hasAction: boolean | undefined,
2971+
) {
29372972
for (const item of items) {
29382973
if (item.name === name) {
29392974
if (documentation !== undefined || text !== undefined) {
@@ -2956,6 +2991,8 @@ Actual: ${stringify(fullActual)}`);
29562991
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
29572992
}
29582993

2994+
assert.equal(item.hasAction, hasAction);
2995+
29592996
return;
29602997
}
29612998
}
@@ -3669,12 +3706,12 @@ namespace FourSlashInterface {
36693706

36703707
// Verifies the completion list contains the specified symbol. The
36713708
// completion list is brought up if necessary
3672-
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
3709+
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
36733710
if (this.negative) {
36743711
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
36753712
}
36763713
else {
3677-
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex);
3714+
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex, hasAction);
36783715
}
36793716
}
36803717

@@ -3999,6 +4036,10 @@ namespace FourSlashInterface {
39994036
this.state.getAndApplyCodeActions(errorCode, index);
40004037
}
40014038

4039+
public applyCodeActionFromCompletion(markerName: string, options: VerifyCompletionActionOptions): void {
4040+
this.state.applyCodeActionFromCompletion(markerName, options);
4041+
}
4042+
40024043
public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void {
40034044
this.state.verifyImportFixAtPosition(expectedTextArray, errorCode);
40044045
}
@@ -4396,12 +4437,20 @@ namespace FourSlashInterface {
43964437
isNewIdentifierLocation?: boolean;
43974438
}
43984439

4399-
export interface VerifyCodeFixOptions {
4400-
description: string;
4401-
// One of these should be defined.
4440+
export interface NewContentOptions {
4441+
// Exactly one of these should be defined.
44024442
newFileContent?: string;
44034443
newRangeContent?: string;
4444+
}
4445+
4446+
export interface VerifyCodeFixOptions extends NewContentOptions {
4447+
description: string;
44044448
errorCode?: number;
44054449
index?: number;
44064450
}
4451+
4452+
export interface VerifyCompletionActionOptions extends NewContentOptions {
4453+
name: string;
4454+
description: string;
4455+
}
44074456
}

src/harness/harnessLanguageService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,8 @@ namespace Harness.LanguageService {
405405
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
406406
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position));
407407
}
408-
getCompletionEntryDetails(fileName: string, position: number, entryName: string): ts.CompletionEntryDetails {
409-
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName));
408+
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: ts.FormatCodeOptions): ts.CompletionEntryDetails {
409+
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(options)));
410410
}
411411
getCompletionEntrySymbol(): ts.Symbol {
412412
throw new Error("getCompletionEntrySymbol not implemented across the shim layer.");

src/server/client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ namespace ts.server {
198198
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetails, args);
199199
const response = this.processResponse<protocol.CompletionDetailsResponse>(request);
200200
Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
201-
return response.body[0];
201+
202+
const convertedCodeActions = map(response.body[0].codeActions, codeAction => this.convertCodeActions(codeAction, fileName));
203+
return { ...response.body[0], codeActions: convertedCodeActions };
202204
}
203205

204206
getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {

0 commit comments

Comments
 (0)