Add 'data' property to completion entry for better coordination between completions and completion details (#42890)

* Add 'data' property to completion entry for better cooperation between completions and completion details

* Add doc comment

* Update API baselines

* Add server test

* Test session’s Full result

* Fix tests

* stableSort to fix server fourslash test

* Explicit verification of data parameter
This commit is contained in:
Andrew Branch
2021-03-01 12:09:45 -08:00
committed by GitHub
parent 41b5abf342
commit 993c503c84
22 changed files with 348 additions and 69 deletions
+1
View File
@@ -600,6 +600,7 @@ namespace ts {
},
tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(escapeLeadingUnderscores(name), symbol),
tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(escapeLeadingUnderscores(name), symbol),
tryFindAmbientModule: moduleName => tryFindAmbientModule(moduleName, /*withAugmentations*/ true),
tryFindAmbientModuleWithoutAugmentations: moduleName => {
// we deliberately exclude augmentations
// since we are only interested in declarations of the module itself
+1
View File
@@ -4165,6 +4165,7 @@ namespace ts {
/* @internal */ createSymbol(flags: SymbolFlags, name: __String): TransientSymbol;
/* @internal */ createIndexInfo(type: Type, isReadonly: boolean, declaration?: SignatureDeclaration): IndexInfo;
/* @internal */ isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
/* @internal */ tryFindAmbientModule(moduleName: string): Symbol | undefined;
/* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined;
/* @internal */ getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker;
+8 -9
View File
@@ -205,9 +205,9 @@ namespace ts.server {
isNewIdentifierLocation: false,
entries: response.body!.map<CompletionEntry>(entry => { // TODO: GH#18217
if (entry.replacementSpan !== undefined) {
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry;
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, data, isRecommended } = entry;
// TODO: GH#241
const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended };
const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, data: data as any, isRecommended };
return res;
}
@@ -216,14 +216,13 @@ namespace ts.server {
};
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails {
const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] };
getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails {
const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] };
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetails, args);
const response = this.processResponse<protocol.CompletionDetailsResponse>(request);
Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body.");
const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) }));
return { ...response.body![0], codeActions: convertedCodeActions };
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetailsFull, args);
const response = this.processResponse<protocol.Response>(request);
Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
return response.body[0];
}
getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {
+19 -9
View File
@@ -399,7 +399,7 @@ namespace FourSlash {
}
const memo = Utils.memoize(
(_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args),
(...args) => args.join("|,|")
(...args) => args.map(a => a && typeof a === "object" ? JSON.stringify(a) : a).join("|,|")
);
proxy[key] = (...args: any[]) => memo(
target.languageServiceAdapterHost.getScriptInfo(target.activeFile.fileName)!.version,
@@ -867,7 +867,7 @@ namespace FourSlash {
nameToEntries.set(entry.name, [entry]);
}
else {
if (entries.some(e => e.source === entry.source)) {
if (entries.some(e => e.source === entry.source && this.deepEqual(e.data, entry.data))) {
this.raiseError(`Duplicate completions for ${entry.name}`);
}
entries.push(entry);
@@ -885,8 +885,8 @@ namespace FourSlash {
const name = typeof include === "string" ? include : include.name;
const found = nameToEntries.get(name);
if (!found) throw this.raiseError(`Includes: completion '${name}' not found.`);
assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`);
this.verifyCompletionEntry(ts.first(found), include);
if (!found.length) throw this.raiseError(`Includes: no completions with name '${name}' remain unmatched.`);
this.verifyCompletionEntry(found.shift()!, include);
}
}
if (options.excludes) {
@@ -933,7 +933,7 @@ namespace FourSlash {
assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`));
if (expected.text !== undefined) {
const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source), `No completion details available for name '${actual.name}' and source '${actual.source}'`);
const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source, actual.data), `No completion details available for name '${actual.name}' and source '${actual.source}'`);
assert.equal(ts.displayPartsToString(actualDetails.displayParts), expected.text, "Expected 'text' property to match 'displayParts' string");
assert.equal(ts.displayPartsToString(actualDetails.documentation), expected.documentation || "", "Expected 'documentation' property to match 'documentation' display parts string");
// TODO: GH#23587
@@ -1254,6 +1254,16 @@ namespace FourSlash {
}
private deepEqual(a: unknown, b: unknown) {
try {
this.assertObjectsEqual(a, b);
return true;
}
catch {
return false;
}
}
public verifyDisplayPartsOfReferencedSymbol(expected: ts.SymbolDisplayPart[]) {
const referencedSymbols = this.findReferencesAtCaret()!;
@@ -1281,11 +1291,11 @@ namespace FourSlash {
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
}
private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
private getCompletionEntryDetails(entryName: string, source: string | undefined, data: ts.CompletionEntryData | undefined, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
if (preferences) {
this.configure(preferences);
}
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences);
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences, data);
}
private getReferencesAtCaret() {
@@ -2796,14 +2806,14 @@ namespace FourSlash {
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
this.goToMarker(markerName);
const details = this.getCompletionEntryDetails(options.name, options.source, options.preferences);
const details = this.getCompletionEntryDetails(options.name, options.source, options.data, options.preferences);
if (!details) {
const completions = this.getCompletionListAtCaret(options.preferences)?.entries;
const matchingName = completions?.filter(e => e.name === options.name);
const detailMessage = matchingName?.length
? `\n Found ${matchingName.length} with name '${options.name}' from source(s) ${matchingName.map(e => `'${e.source}'`).join(", ")}.`
: ` (In fact, there were no completions with name '${options.name}' at all.)`;
return this.raiseError(`No completions were found for the given name, source, and preferences.` + detailMessage);
return this.raiseError(`No completions were found for the given name, source/data, and preferences.` + detailMessage);
}
const codeActions = details.codeActions;
if (codeActions?.length !== 1) {
+1
View File
@@ -1701,6 +1701,7 @@ namespace FourSlashInterface {
export interface VerifyCompletionActionOptions extends NewContentOptions {
name: string;
source?: string;
data?: ts.CompletionEntryData;
description: string;
preferences?: ts.UserPreferences;
}
+2 -2
View File
@@ -474,8 +474,8 @@ namespace Harness.LanguageService {
getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences));
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences));
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data));
}
getCompletionEntrySymbol(): ts.Symbol {
throw new Error("getCompletionEntrySymbol not implemented across the shim layer.");
+7
View File
@@ -2169,6 +2169,7 @@ namespace ts.server.protocol {
export interface CompletionEntryIdentifier {
name: string;
source?: string;
data?: unknown;
}
/**
@@ -2255,6 +2256,12 @@ namespace ts.server.protocol {
* in the project package.json.
*/
isPackageJsonImport?: true;
/**
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
* items with the same name.
*/
data?: unknown;
}
/**
+14 -6
View File
@@ -1808,14 +1808,14 @@ namespace ts.server {
if (kind === protocol.CommandTypes.CompletionsFull) return completions;
const prefix = args.prefix || "";
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
const entries = stableSort(mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport } = entry;
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport, data } = entry;
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport };
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport, data };
}
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
}), (a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
if (kind === protocol.CommandTypes.Completions) {
if (completions.metadata) (entries as WithMetadata<readonly protocol.CompletionEntry[]>).metadata = completions.metadata;
@@ -1837,8 +1837,8 @@ namespace ts.server {
const formattingOptions = project.projectService.getFormatCodeOptions(file);
const result = mapDefined(args.entryNames, entryName => {
const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName;
return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file));
const { name, source, data } = typeof entryName === "string" ? { name: entryName, source: undefined, data: undefined } : entryName;
return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file), data ? cast(data, isCompletionEntryData) : undefined);
});
return simplifiedResult
? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(action)) }))
@@ -3118,4 +3118,12 @@ namespace ts.server {
isDefinition
};
}
function isCompletionEntryData(data: any): data is CompletionEntryData {
return data === undefined || data && typeof data === "object"
&& typeof data.exportName === "string"
&& (data.fileName === undefined || typeof data.fileName === "string")
&& (data.ambientModuleName === undefined || typeof data.ambientModuleName === "string"
&& (data.isPackageJsonImport === undefined || typeof data.isPackageJsonImport === "boolean"));
}
}
+78 -22
View File
@@ -48,6 +48,8 @@ namespace ts.Completions {
moduleSymbol: Symbol;
isDefaultExport: boolean;
isFromPackageJson?: boolean;
exportName: string;
fileName?: string;
}
function originIsThisType(origin: SymbolOriginInfo): boolean {
@@ -104,7 +106,6 @@ namespace ts.Completions {
export interface AutoImportSuggestion {
symbol: Symbol;
symbolName: string;
skipFilter: boolean;
origin: SymbolOriginInfoExport;
}
export interface ImportSuggestionsForFileCache {
@@ -422,6 +423,7 @@ namespace ts.Completions {
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(contextToken);
let data: CompletionEntryData | undefined;
const insertQuestionDot = origin && originIsNullableMember(origin);
const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess;
@@ -472,6 +474,15 @@ namespace ts.Completions {
return undefined;
}
if (originIsExport(origin)) {
data = {
exportName: origin.exportName,
fileName: origin.fileName,
ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name),
isPackageJsonImport: origin.isFromPackageJson ? true : undefined,
};
}
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
// 'getSymbolKind' which is permissible given that it is backwards compatible; but
// really we should consider passing the meaning for the node so that we don't report
@@ -491,6 +502,7 @@ namespace ts.Completions {
insertText,
replacementSpan,
isPackageJsonImport: originIsPackageJsonImport(origin) || undefined,
data,
};
}
@@ -669,6 +681,7 @@ namespace ts.Completions {
export interface CompletionEntryIdentifier {
name: string;
source?: string;
data?: CompletionEntryData;
}
export function getCompletionEntryDetails(
@@ -1485,25 +1498,35 @@ namespace ts.Completions {
if (shouldOfferImportCompletions()) {
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host);
if (!detailsEntryId && importSuggestionsCache) {
importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion());
if (detailsEntryId?.data) {
const autoImport = getAutoImportSymbolFromCompletionEntryData(detailsEntryId.data);
if (autoImport) {
const symbolId = getSymbolId(autoImport.symbol);
symbols.push(autoImport.symbol);
symbolToOriginInfoMap[symbolId] = autoImport.origin;
}
}
autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => {
if (detailsEntryId) {
if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) {
else {
const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host);
if (!detailsEntryId && importSuggestionsCache) {
importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion());
}
autoImportSuggestions.forEach(({ symbol, symbolName, origin }) => {
if (detailsEntryId) {
if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) {
return;
}
}
else if (!stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) {
return;
}
}
else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) {
return;
}
const symbolId = getSymbolId(symbol);
symbols.push(symbol);
symbolToOriginInfoMap[symbolId] = origin;
symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions;
});
const symbolId = getSymbolId(symbol);
symbols.push(symbol);
symbolToOriginInfoMap[symbolId] = origin;
symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions;
});
}
}
filterGlobalCompletion(symbols);
}
@@ -1673,7 +1696,7 @@ namespace ts.Completions {
const seenResolvedModules = new Map<SymbolId, true>();
const results = createMultiMap<SymbolId, AutoImportSuggestion>();
codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, /*useAutoImportProvider*/ true, (moduleSymbol, _, program, isFromPackageJson) => {
codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, /*useAutoImportProvider*/ true, (moduleSymbol, file, program, isFromPackageJson) => {
// Perf -- ignore other modules if this is a request for details
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
return;
@@ -1689,7 +1712,7 @@ namespace ts.Completions {
// Don't add another completion for `export =` of a symbol that's already global.
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
if (resolvedModuleSymbol !== moduleSymbol && every(resolvedModuleSymbol.declarations, isNonGlobalDeclaration)) {
pushSymbol(resolvedModuleSymbol, moduleSymbol, isFromPackageJson, /*skipFilter*/ true);
pushSymbol(resolvedModuleSymbol, InternalSymbolName.ExportEquals, moduleSymbol, file, isFromPackageJson);
}
for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) {
@@ -1698,14 +1721,14 @@ namespace ts.Completions {
continue;
}
pushSymbol(symbol, moduleSymbol, isFromPackageJson, /*skipFilter*/ false);
pushSymbol(symbol, symbol.name, moduleSymbol, file, isFromPackageJson);
}
});
log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`);
return flatten(arrayFrom(results.values()));
function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, isFromPackageJson: boolean, skipFilter: boolean) {
function pushSymbol(symbol: Symbol, exportName: string, moduleSymbol: Symbol, file: SourceFile | undefined, isFromPackageJson: boolean) {
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
const nonLocalSymbol = symbol;
if (isDefaultExport) {
@@ -1718,17 +1741,50 @@ namespace ts.Completions {
const symbolName = getNameForExportedSymbol(symbol, target);
const existingSuggestions = results.get(getSymbolId(original));
if (!some(existingSuggestions, s => s.symbolName === symbolName && moduleSymbolsAreDuplicateOrigins(moduleSymbol, s.origin.moduleSymbol))) {
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport, isFromPackageJson };
const origin: SymbolOriginInfoExport = {
kind: SymbolOriginInfoKind.Export,
moduleSymbol,
isDefaultExport,
isFromPackageJson,
exportName,
fileName: file?.fileName
};
results.add(getSymbolId(original), {
symbol,
symbolName,
origin,
skipFilter,
});
}
}
}
function getAutoImportSymbolFromCompletionEntryData(data: CompletionEntryData): { symbol: Symbol, origin: SymbolOriginInfoExport } | undefined {
const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program;
const checker = containingProgram.getTypeChecker();
const moduleSymbol =
data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) :
data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) :
undefined;
if (!moduleSymbol) return undefined;
let symbol = data.exportName === InternalSymbolName.ExportEquals
? checker.resolveExternalModuleSymbol(moduleSymbol)
: checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol);
if (!symbol) return undefined;
const isDefaultExport = data.exportName === InternalSymbolName.Default;
symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol;
return {
symbol,
origin: {
kind: SymbolOriginInfoKind.Export,
moduleSymbol,
isDefaultExport,
exportName: data.exportName,
fileName: data.fileName,
}
};
}
/**
* Determines whether a module symbol is redundant with another for purposes of offering
* auto-import completions for exports of the same symbol. Exports of the same symbol
+2 -2
View File
@@ -1558,14 +1558,14 @@ namespace ts {
options.triggerCharacter);
}
function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined {
function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions, data?: CompletionEntryData): CompletionEntryDetails | undefined {
synchronizeHostData();
return Completions.getCompletionEntryDetails(
program,
log,
getValidSourceFile(fileName),
position,
{ name, source },
{ name, source, data },
host,
(formattingOptions && formatting.getFormatContext(formattingOptions, host))!, // TODO: GH#18217
preferences,
+3 -3
View File
@@ -150,7 +150,7 @@ namespace ts {
getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: SemanticClassificationFormat): string;
getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): string;
getQuickInfoAtPosition(fileName: string, position: number): string;
@@ -962,12 +962,12 @@ namespace ts {
}
/** Get a string based representation of a completion list entry details */
public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined) {
public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined) {
return this.forwardJSONCall(
`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`,
() => {
const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions);
return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences);
return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences, data);
}
);
}
+27 -2
View File
@@ -424,10 +424,11 @@ namespace ts {
*
* @param fileName The path to the file
* @param position A zero based index of the character where you want the entries
* @param entryName The name from an existing completion which came from `getCompletionsAtPosition`
* @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition`
* @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility
* @param source Source code for the current file, can be undefined for backwards compatibility
* @param source `source` property from the completion entry
* @param preferences User settings, can be undefined for backwards compatibility
* @param data `data` property from the completion entry
*/
getCompletionEntryDetails(
fileName: string,
@@ -436,6 +437,7 @@ namespace ts {
formatOptions: FormatCodeOptions | FormatCodeSettings | undefined,
source: string | undefined,
preferences: UserPreferences | undefined,
data: CompletionEntryData | undefined,
): CompletionEntryDetails | undefined;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
@@ -1136,6 +1138,20 @@ namespace ts {
entries: CompletionEntry[];
}
export interface CompletionEntryData {
/** The file name declaring the export's module symbol, if it was an external module */
fileName?: string;
/** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */
ambientModuleName?: string;
/** True if the export was found in the package.json AutoImportProvider */
isPackageJsonImport?: true;
/**
* The name of the property or export in the module's symbol table. Differs from the completion name
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
}
// see comments in protocol.ts
export interface CompletionEntry {
name: string;
@@ -1154,6 +1170,15 @@ namespace ts {
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
/**
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
* items with the same name. Currently only defined for auto-import completions, but the type is
* `unknown` in the protocol, so it can be changed as needed to support other kinds of completions.
* The presence of this property should generally not be used to assume that this completion entry
* is an auto-import.
*/
data?: CompletionEntryData;
}
export interface CompletionEntryDetails {
@@ -47,7 +47,7 @@ namespace ts {
placeOpenBraceOnNewLineForControlBlocks: false,
};
verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for completion entry details, so the first check is within the checker
service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*content*/ undefined, {})!, r => assert.exists(r.displayParts)
service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*source*/ undefined, {}, /*data*/ undefined)!, r => assert.exists(r.displayParts)
);
});
@@ -39,6 +39,7 @@ namespace ts.projectSystem {
isPackageJsonImport: undefined,
sortText: Completions.SortText.AutoImportSuggestions,
source: "/a",
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }
};
assert.deepEqual<protocol.CompletionInfo | undefined>(response, {
isGlobalCompletion: true,
@@ -50,7 +51,7 @@ namespace ts.projectSystem {
const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = {
...requestLocation,
entryNames: [{ name: "foo", source: "/a" }],
entryNames: [{ name: "foo", source: "/a", data: { exportName: "foo", fileName: "/a.ts" } }],
};
const detailsResponse = executeSessionRequest<protocol.CompletionDetailsRequest, protocol.CompletionDetailsResponse>(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs);
@@ -70,7 +70,8 @@ import { something } from "something";
isPackageJsonImport: undefined,
isRecommended: undefined,
replacementSpan: undefined,
source: undefined
source: undefined,
data: undefined,
};
}
});
+33 -3
View File
@@ -5528,12 +5528,13 @@ declare namespace ts {
*
* @param fileName The path to the file
* @param position A zero based index of the character where you want the entries
* @param entryName The name from an existing completion which came from `getCompletionsAtPosition`
* @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition`
* @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility
* @param source Source code for the current file, can be undefined for backwards compatibility
* @param source `source` property from the completion entry
* @param preferences User settings, can be undefined for backwards compatibility
* @param data `data` property from the completion entry
*/
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
/**
* Gets semantic information about the identifier at a particular position in a
@@ -6093,6 +6094,19 @@ declare namespace ts {
isNewIdentifierLocation: boolean;
entries: CompletionEntry[];
}
interface CompletionEntryData {
/** The file name declaring the export's module symbol, if it was an external module */
fileName?: string;
/** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */
ambientModuleName?: string;
/** True if the export was found in the package.json AutoImportProvider */
isPackageJsonImport?: true;
/**
* The name of the property or export in the module's symbol table. Differs from the completion name
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
}
interface CompletionEntry {
name: string;
kind: ScriptElementKind;
@@ -6110,6 +6124,15 @@ declare namespace ts {
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
/**
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
* items with the same name. Currently only defined for auto-import completions, but the type is
* `unknown` in the protocol, so it can be changed as needed to support other kinds of completions.
* The presence of this property should generally not be used to assume that this completion entry
* is an auto-import.
*/
data?: CompletionEntryData;
}
interface CompletionEntryDetails {
name: string;
@@ -8162,6 +8185,7 @@ declare namespace ts.server.protocol {
interface CompletionEntryIdentifier {
name: string;
source?: string;
data?: unknown;
}
/**
* Completion entry details request; value of command field is
@@ -8244,6 +8268,12 @@ declare namespace ts.server.protocol {
* in the project package.json.
*/
isPackageJsonImport?: true;
/**
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
* items with the same name.
*/
data?: unknown;
}
/**
* Additional completion entry details, available on demand
+26 -3
View File
@@ -5528,12 +5528,13 @@ declare namespace ts {
*
* @param fileName The path to the file
* @param position A zero based index of the character where you want the entries
* @param entryName The name from an existing completion which came from `getCompletionsAtPosition`
* @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition`
* @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility
* @param source Source code for the current file, can be undefined for backwards compatibility
* @param source `source` property from the completion entry
* @param preferences User settings, can be undefined for backwards compatibility
* @param data `data` property from the completion entry
*/
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
/**
* Gets semantic information about the identifier at a particular position in a
@@ -6093,6 +6094,19 @@ declare namespace ts {
isNewIdentifierLocation: boolean;
entries: CompletionEntry[];
}
interface CompletionEntryData {
/** The file name declaring the export's module symbol, if it was an external module */
fileName?: string;
/** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */
ambientModuleName?: string;
/** True if the export was found in the package.json AutoImportProvider */
isPackageJsonImport?: true;
/**
* The name of the property or export in the module's symbol table. Differs from the completion name
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
}
interface CompletionEntry {
name: string;
kind: ScriptElementKind;
@@ -6110,6 +6124,15 @@ declare namespace ts {
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
/**
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
* items with the same name. Currently only defined for auto-import completions, but the type is
* `unknown` in the protocol, so it can be changed as needed to support other kinds of completions.
* The presence of this property should generally not be used to assume that this completion entry
* is an auto-import.
*/
data?: CompletionEntryData;
}
interface CompletionEntryDetails {
name: string;
@@ -6,7 +6,7 @@
/////*3*/
verify.completions(
{ marker: "0", includes: ["B", "\u0042"] },
{ marker: "2", excludes: ["C", "\u0043", "A", "\u0041"], isNewIdentifierLocation: true },
{ marker: "3", includes: ["B", "\u0042", "A", "\u0041", "C", "\u0043"] },
{ marker: "0", includes: ["B"] },
{ marker: "2", excludes: ["C", "A"], isNewIdentifierLocation: true },
{ marker: "3", includes: ["B", "A", "C"] },
);
@@ -0,0 +1,52 @@
/// <reference path="fourslash.ts" />
// @noLib: true
// @Filename: /someModule.ts
//// export const someModule = 0;
//// export default 1;
// @Filename: /index.ts
//// someMo/**/
verify.completions({
marker: "",
exact: [
completion.globalThisEntry,
completion.undefinedVarEntry,
{
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "const someModule: 0",
kind: "const",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
},
{
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "(property) default: 1",
kind: "property",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
},
...completion.statementKeywordsWithTypes
],
preferences: {
includeCompletionsForModuleExports: true
}
});
verify.applyCodeActionFromCompletion("", {
name: "someModule",
source: "/someModule",
data: { exportName: "default", fileName: "/someModule.ts" },
description: `Import default 'someModule' from module "./someModule"`,
newFileContent: `import someModule from "./someModule";
someMo`
});
@@ -28,12 +28,20 @@ verify.completions(
exact: [
completion.globalThisEntry,
completion.undefinedVarEntry,
exportEntry,
...completion.statementKeywordsWithTypes
],
preferences
},
{ marker: "1", includes: exportEntry, preferences }
{
marker: "1",
exact: [
completion.globalThisEntry,
completion.undefinedVarEntry,
exportEntry,
...completion.statementKeywordsWithTypes
],
preferences
}
);
verify.applyCodeActionFromCompletion("0", {
name: "fooBar",
+8
View File
@@ -98,6 +98,13 @@ declare module ts {
character: number;
}
interface CompletionEntryData {
fileName?: string;
ambientModuleName?: string;
isPackageJsonImport?: true;
exportName: string;
}
function flatMap<T, U>(array: ReadonlyArray<T>, mapfn: (x: T, i: number) => U | ReadonlyArray<U> | undefined): U[];
}
@@ -253,6 +260,7 @@ declare namespace FourSlashInterface {
applyCodeActionFromCompletion(markerName: string, options: {
name: string,
source?: string,
data?: ts.CompletionEntryData,
description: string,
newFileContent?: string,
newRangeContent?: string,
@@ -0,0 +1,48 @@
/// <reference path="../fourslash.ts" />
// @Filename: /tsconfig.json
//// { "compilerOptions": { "noLib": true } }
// @Filename: /someModule.ts
//// export const someModule = 0;
//// export default 1;
// @Filename: /index.ts
//// someMo/**/
verify.completions({
marker: "",
includes: [
{
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "const someModule: 0",
kind: "const",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
},
{
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "(property) default: 1",
kind: "property",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
},
],
preferences: {
includeCompletionsForModuleExports: true
}
});
verify.applyCodeActionFromCompletion("", {
name: "someModule",
source: "/someModule",
data: { exportName: "default", fileName: "/someModule.ts" },
description: `Import default 'someModule' from module "./someModule"`,
newFileContent: `import someModule from "./someModule";\r\n\r\nsomeMo`
});