mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-26 00:36:29 -05:00
2060 lines
112 KiB
TypeScript
2060 lines
112 KiB
TypeScript
/* @internal */
|
|
namespace ts.Completions {
|
|
export type Log = (message: string) => void;
|
|
|
|
const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export }
|
|
type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport;
|
|
interface SymbolOriginInfoExport {
|
|
kind: SymbolOriginInfoKind.SymbolMemberExport | SymbolOriginInfoKind.Export;
|
|
moduleSymbol: Symbol;
|
|
isDefaultExport: boolean;
|
|
}
|
|
function originIsSymbolMember(origin: SymbolOriginInfo): boolean {
|
|
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.SymbolMemberNoExport;
|
|
}
|
|
function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport {
|
|
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export;
|
|
}
|
|
|
|
/**
|
|
* Map from symbol id -> SymbolOriginInfo.
|
|
* Only populated for symbols that come from other modules.
|
|
*/
|
|
type SymbolOriginInfoMap = (SymbolOriginInfo | undefined)[];
|
|
|
|
const enum KeywordCompletionFilters {
|
|
None, // No keywords
|
|
All, // Every possible keyword (TODO: This is never appropriate)
|
|
ClassElementKeywords, // Keywords inside class body
|
|
InterfaceElementKeywords, // Keywords inside interface body
|
|
ConstructorParameterKeywords, // Keywords at constructor parameter
|
|
FunctionLikeBodyKeywords, // Keywords at function like body
|
|
TypeKeywords,
|
|
}
|
|
|
|
const enum GlobalsSearch { Continue, Success, Fail }
|
|
|
|
export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences, triggerCharacter: CompletionsTriggerCharacter | undefined): CompletionInfo | undefined {
|
|
const typeChecker = program.getTypeChecker();
|
|
const compilerOptions = program.getCompilerOptions();
|
|
|
|
const contextToken = findPrecedingToken(position, sourceFile);
|
|
if (triggerCharacter && !isValidTrigger(sourceFile, triggerCharacter, contextToken, position)) return undefined;
|
|
|
|
const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, contextToken, typeChecker, compilerOptions, host, log, preferences);
|
|
if (stringCompletions) {
|
|
return stringCompletions;
|
|
}
|
|
|
|
if (contextToken && isBreakOrContinueStatement(contextToken.parent)
|
|
&& (contextToken.kind === SyntaxKind.BreakKeyword || contextToken.kind === SyntaxKind.ContinueKeyword || contextToken.kind === SyntaxKind.Identifier)) {
|
|
return getLabelCompletionAtPosition(contextToken.parent);
|
|
}
|
|
|
|
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined);
|
|
if (!completionData) {
|
|
return undefined;
|
|
}
|
|
|
|
switch (completionData.kind) {
|
|
case CompletionDataKind.Data:
|
|
return completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, preferences);
|
|
case CompletionDataKind.JsDocTagName:
|
|
// If the current position is a jsDoc tag name, only tag names should be provided for completion
|
|
return jsdocCompletionInfo(JsDoc.getJSDocTagNameCompletions());
|
|
case CompletionDataKind.JsDocTag:
|
|
// If the current position is a jsDoc tag, only tags should be provided for completion
|
|
return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions());
|
|
case CompletionDataKind.JsDocParameterName:
|
|
return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag));
|
|
default:
|
|
return Debug.assertNever(completionData);
|
|
}
|
|
}
|
|
|
|
function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
|
|
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
|
}
|
|
|
|
function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined {
|
|
const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData;
|
|
|
|
if (location && location.parent && isJsxClosingElement(location.parent)) {
|
|
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
|
|
// instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element.
|
|
// For example:
|
|
// var x = <div> </ /*1*/
|
|
// The completion list at "1" will contain "div>" with type any
|
|
// And at `<div> </ /*1*/ >` (with a closing `>`), the completion list will contain "div".
|
|
const tagName = location.parent.parent.openingElement.tagName;
|
|
const hasClosingAngleBracket = !!findChildOfKind(location.parent, SyntaxKind.GreaterThanToken, sourceFile);
|
|
const entry: CompletionEntry = {
|
|
name: tagName.getFullText(sourceFile) + (hasClosingAngleBracket ? "" : ">"),
|
|
kind: ScriptElementKind.classElement,
|
|
kindModifiers: undefined,
|
|
sortText: "0",
|
|
};
|
|
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: [entry] };
|
|
}
|
|
|
|
const entries: CompletionEntry[] = [];
|
|
|
|
if (isUncheckedFile(sourceFile, compilerOptions)) {
|
|
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target!, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap);
|
|
getJSCompletionEntries(sourceFile, location!.pos, uniqueNames, compilerOptions.target!, entries); // TODO: GH#18217
|
|
}
|
|
else {
|
|
if ((!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
|
|
return undefined;
|
|
}
|
|
|
|
getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target!, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap);
|
|
}
|
|
|
|
if (keywordFilters !== KeywordCompletionFilters.None) {
|
|
const entryNames = arrayToSet(entries, e => e.name);
|
|
for (const keywordEntry of getKeywordCompletions(keywordFilters)) {
|
|
if (!entryNames.has(keywordEntry.name)) {
|
|
entries.push(keywordEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const literal of literals) {
|
|
entries.push(createCompletionEntryForLiteral(literal));
|
|
}
|
|
|
|
return { isGlobalCompletion: isInSnippetScope, isMemberCompletion: isMemberCompletionKind(completionKind), isNewIdentifierLocation, entries };
|
|
}
|
|
|
|
function isUncheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean {
|
|
return isSourceFileJS(sourceFile) && !isCheckJsEnabledForFile(sourceFile, compilerOptions);
|
|
}
|
|
|
|
function isMemberCompletionKind(kind: CompletionKind): boolean {
|
|
switch (kind) {
|
|
case CompletionKind.ObjectPropertyDeclaration:
|
|
case CompletionKind.MemberLike:
|
|
case CompletionKind.PropertyAccess:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getJSCompletionEntries(
|
|
sourceFile: SourceFile,
|
|
position: number,
|
|
uniqueNames: Map<true>,
|
|
target: ScriptTarget,
|
|
entries: Push<CompletionEntry>): void {
|
|
getNameTable(sourceFile).forEach((pos, name) => {
|
|
// Skip identifiers produced only from the current location
|
|
if (pos === position) {
|
|
return;
|
|
}
|
|
const realName = unescapeLeadingUnderscores(name);
|
|
if (addToSeen(uniqueNames, realName) && isIdentifierText(realName, target)) {
|
|
entries.push({
|
|
name: realName,
|
|
kind: ScriptElementKind.warning,
|
|
kindModifiers: "",
|
|
sortText: "1"
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const completionNameForLiteral = (literal: string | number | PseudoBigInt) =>
|
|
typeof literal === "object" ? pseudoBigIntToString(literal) + "n" : JSON.stringify(literal);
|
|
function createCompletionEntryForLiteral(literal: string | number | PseudoBigInt): CompletionEntry {
|
|
return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: "0" };
|
|
}
|
|
|
|
function createCompletionEntry(
|
|
symbol: Symbol,
|
|
location: Node | undefined,
|
|
sourceFile: SourceFile,
|
|
typeChecker: TypeChecker,
|
|
target: ScriptTarget,
|
|
kind: CompletionKind,
|
|
origin: SymbolOriginInfo | undefined,
|
|
recommendedCompletion: Symbol | undefined,
|
|
propertyAccessToConvert: PropertyAccessExpression | undefined,
|
|
isJsxInitializer: IsJsxInitializer | undefined,
|
|
preferences: UserPreferences,
|
|
): CompletionEntry | undefined {
|
|
const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind);
|
|
if (!info) {
|
|
return undefined;
|
|
}
|
|
const { name, needsConvertPropertyAccess } = info;
|
|
|
|
let insertText: string | undefined;
|
|
let replacementSpan: TextSpan | undefined;
|
|
if (origin && origin.kind === SymbolOriginInfoKind.ThisType) {
|
|
insertText = needsConvertPropertyAccess ? `this[${quote(name, preferences)}]` : `this.${name}`;
|
|
}
|
|
// We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790.
|
|
// Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro.
|
|
else if ((origin && originIsSymbolMember(origin) || needsConvertPropertyAccess) && propertyAccessToConvert) {
|
|
insertText = needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]`;
|
|
const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!;
|
|
// If the text after the '.' starts with this name, write over it. Else, add new text.
|
|
const end = startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end;
|
|
replacementSpan = createTextSpanFromBounds(dot.getStart(sourceFile), end);
|
|
}
|
|
|
|
if (isJsxInitializer) {
|
|
if (insertText === undefined) insertText = name;
|
|
insertText = `{${insertText}}`;
|
|
if (typeof isJsxInitializer !== "boolean") {
|
|
replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile);
|
|
}
|
|
}
|
|
|
|
if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) {
|
|
return 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
|
|
// that a suggestion for a value is an interface. We COULD also just do what
|
|
// 'getSymbolModifiers' does, which is to use the first declaration.
|
|
|
|
// Use a 'sortText' of 0' so that all symbol completion entries come before any other
|
|
// entries (like JavaScript identifier entries).
|
|
return {
|
|
name,
|
|
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location!), // TODO: GH#18217
|
|
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
|
|
sortText: "0",
|
|
source: getSourceFromOrigin(origin),
|
|
hasAction: trueOrUndefined(!!origin && originIsExport(origin)),
|
|
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
|
|
insertText,
|
|
replacementSpan,
|
|
};
|
|
}
|
|
|
|
function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol | undefined, checker: TypeChecker): boolean {
|
|
return localSymbol === recommendedCompletion ||
|
|
!!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion;
|
|
}
|
|
|
|
function trueOrUndefined(b: boolean): true | undefined {
|
|
return b ? true : undefined;
|
|
}
|
|
|
|
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
|
|
return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined;
|
|
}
|
|
|
|
export function getCompletionEntriesFromSymbols(
|
|
symbols: ReadonlyArray<Symbol>,
|
|
entries: Push<CompletionEntry>,
|
|
location: Node | undefined,
|
|
sourceFile: SourceFile,
|
|
typeChecker: TypeChecker,
|
|
target: ScriptTarget,
|
|
log: Log,
|
|
kind: CompletionKind,
|
|
preferences: UserPreferences,
|
|
propertyAccessToConvert?: PropertyAccessExpression | undefined,
|
|
isJsxInitializer?: IsJsxInitializer,
|
|
recommendedCompletion?: Symbol,
|
|
symbolToOriginInfoMap?: SymbolOriginInfoMap,
|
|
): Map<true> {
|
|
const start = timestamp();
|
|
// Tracks unique names.
|
|
// We don't set this for global variables or completions from external module exports, because we can have multiple of those.
|
|
// Based on the order we add things we will always see locals first, then globals, then module exports.
|
|
// So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name.
|
|
const uniques = createMap<true>();
|
|
for (const symbol of symbols) {
|
|
const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined;
|
|
const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, preferences);
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
|
|
const { name } = entry;
|
|
if (uniques.has(name)) {
|
|
continue;
|
|
}
|
|
|
|
// Latter case tests whether this is a global variable.
|
|
if (!origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile()))) { // TODO: GH#18217
|
|
uniques.set(name, true);
|
|
}
|
|
|
|
entries.push(entry);
|
|
}
|
|
|
|
log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start));
|
|
return uniques;
|
|
}
|
|
|
|
function getLabelCompletionAtPosition(node: BreakOrContinueStatement): CompletionInfo | undefined {
|
|
const entries = getLabelStatementCompletions(node);
|
|
if (entries.length) {
|
|
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
|
}
|
|
}
|
|
|
|
function getLabelStatementCompletions(node: Node): CompletionEntry[] {
|
|
const entries: CompletionEntry[] = [];
|
|
const uniques = createMap<true>();
|
|
let current = node;
|
|
|
|
while (current) {
|
|
if (isFunctionLike(current)) {
|
|
break;
|
|
}
|
|
if (isLabeledStatement(current)) {
|
|
const name = current.label.text;
|
|
if (!uniques.has(name)) {
|
|
uniques.set(name, true);
|
|
entries.push({
|
|
name,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
kind: ScriptElementKind.label,
|
|
sortText: "0"
|
|
});
|
|
}
|
|
}
|
|
current = current.parent;
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
interface SymbolCompletion {
|
|
type: "symbol";
|
|
symbol: Symbol;
|
|
location: Node | undefined;
|
|
symbolToOriginInfoMap: SymbolOriginInfoMap;
|
|
previousToken: Node | undefined;
|
|
readonly isJsxInitializer: IsJsxInitializer;
|
|
}
|
|
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
|
|
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } {
|
|
const compilerOptions = program.getCompilerOptions();
|
|
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
|
|
if (!completionData) {
|
|
return { type: "none" };
|
|
}
|
|
if (completionData.kind !== CompletionDataKind.Data) {
|
|
return { type: "request", request: completionData };
|
|
}
|
|
|
|
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
|
|
|
|
const literal = find(literals, l => completionNameForLiteral(l) === entryId.name);
|
|
if (literal !== undefined) return { type: "literal", literal };
|
|
|
|
// Find the symbol with the matching entry name.
|
|
// We don't need to perform character checks here because we're only comparing the
|
|
// name against 'entryName' (which is known to be good), not building a new
|
|
// completion entry.
|
|
return firstDefined<Symbol, SymbolCompletion>(symbols, (symbol): SymbolCompletion | undefined => { // TODO: Shouldn't need return type annotation (GH#12632)
|
|
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
|
|
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind);
|
|
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
|
|
? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
|
|
: undefined;
|
|
}) || { type: "none" };
|
|
}
|
|
|
|
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
|
|
return origin && originIsExport(origin) && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
|
|
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
|
|
? firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined)
|
|
|| codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target)
|
|
: symbol.name;
|
|
}
|
|
|
|
export interface CompletionEntryIdentifier {
|
|
name: string;
|
|
source?: string;
|
|
}
|
|
|
|
export function getCompletionEntryDetails(
|
|
program: Program,
|
|
log: Log,
|
|
sourceFile: SourceFile,
|
|
position: number,
|
|
entryId: CompletionEntryIdentifier,
|
|
host: LanguageServiceHost,
|
|
formatContext: formatting.FormatContext,
|
|
preferences: UserPreferences,
|
|
cancellationToken: CancellationToken,
|
|
): CompletionEntryDetails | undefined {
|
|
const typeChecker = program.getTypeChecker();
|
|
const compilerOptions = program.getCompilerOptions();
|
|
const { name } = entryId;
|
|
|
|
const contextToken = findPrecedingToken(position, sourceFile);
|
|
if (isInString(sourceFile, position, contextToken)) {
|
|
return StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken);
|
|
}
|
|
|
|
// Compute all the completion symbols again.
|
|
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
|
|
switch (symbolCompletion.type) {
|
|
case "request": {
|
|
const { request } = symbolCompletion;
|
|
switch (request.kind) {
|
|
case CompletionDataKind.JsDocTagName:
|
|
return JsDoc.getJSDocTagNameCompletionDetails(name);
|
|
case CompletionDataKind.JsDocTag:
|
|
return JsDoc.getJSDocTagCompletionDetails(name);
|
|
case CompletionDataKind.JsDocParameterName:
|
|
return JsDoc.getJSDocParameterNameCompletionDetails(name);
|
|
default:
|
|
return Debug.assertNever(request);
|
|
}
|
|
}
|
|
case "symbol": {
|
|
const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion;
|
|
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences);
|
|
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
|
|
}
|
|
case "literal": {
|
|
const { literal } = symbolCompletion;
|
|
return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
|
|
}
|
|
case "none":
|
|
// Didn't find a symbol with this name. See if we can find a keyword instead.
|
|
return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
|
|
default:
|
|
Debug.assertNever(symbolCompletion);
|
|
}
|
|
}
|
|
|
|
function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails {
|
|
return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]);
|
|
}
|
|
|
|
export function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails {
|
|
const { displayParts, documentation, symbolKind, tags } =
|
|
checker.runWithCancellationToken(cancellationToken, checker =>
|
|
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, SemanticMeaning.All)
|
|
);
|
|
return createCompletionDetails(symbol.name, SymbolDisplay.getSymbolModifiers(symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay);
|
|
}
|
|
|
|
export function createCompletionDetails(name: string, kindModifiers: string, kind: ScriptElementKind, displayParts: SymbolDisplayPart[], documentation?: SymbolDisplayPart[], tags?: JSDocTagInfo[], codeActions?: CodeAction[], source?: SymbolDisplayPart[]): CompletionEntryDetails {
|
|
return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source };
|
|
}
|
|
|
|
interface CodeActionsAndSourceDisplay {
|
|
readonly codeActions: CodeAction[] | undefined;
|
|
readonly sourceDisplay: SymbolDisplayPart[] | undefined;
|
|
}
|
|
function getCompletionEntryCodeActionsAndSourceDisplay(
|
|
symbolToOriginInfoMap: SymbolOriginInfoMap,
|
|
symbol: Symbol,
|
|
program: Program,
|
|
checker: TypeChecker,
|
|
host: LanguageServiceHost,
|
|
compilerOptions: CompilerOptions,
|
|
sourceFile: SourceFile,
|
|
position: number,
|
|
previousToken: Node | undefined,
|
|
formatContext: formatting.FormatContext,
|
|
preferences: UserPreferences,
|
|
): CodeActionsAndSourceDisplay {
|
|
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
|
|
if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) {
|
|
return { codeActions: undefined, sourceDisplay: undefined };
|
|
}
|
|
|
|
const { moduleSymbol } = symbolOriginInfo;
|
|
const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker));
|
|
const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction(
|
|
exportedSymbol,
|
|
moduleSymbol,
|
|
sourceFile,
|
|
getSymbolName(symbol, symbolOriginInfo, compilerOptions.target!),
|
|
host,
|
|
program,
|
|
formatContext,
|
|
previousToken && isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position,
|
|
preferences);
|
|
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
|
|
}
|
|
|
|
export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined {
|
|
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
|
|
return completion.type === "symbol" ? completion.symbol : undefined;
|
|
}
|
|
|
|
const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName }
|
|
/** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */
|
|
type IsJsxInitializer = boolean | Identifier;
|
|
interface CompletionData {
|
|
readonly kind: CompletionDataKind.Data;
|
|
readonly symbols: ReadonlyArray<Symbol>;
|
|
readonly completionKind: CompletionKind;
|
|
readonly isInSnippetScope: boolean;
|
|
/** Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. */
|
|
readonly propertyAccessToConvert: PropertyAccessExpression | undefined;
|
|
readonly isNewIdentifierLocation: boolean;
|
|
readonly location: Node | undefined;
|
|
readonly keywordFilters: KeywordCompletionFilters;
|
|
readonly literals: ReadonlyArray<string | number | PseudoBigInt>;
|
|
readonly symbolToOriginInfoMap: SymbolOriginInfoMap;
|
|
readonly recommendedCompletion: Symbol | undefined;
|
|
readonly previousToken: Node | undefined;
|
|
readonly isJsxInitializer: IsJsxInitializer;
|
|
}
|
|
type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag };
|
|
|
|
export const enum CompletionKind {
|
|
ObjectPropertyDeclaration,
|
|
Global,
|
|
PropertyAccess,
|
|
MemberLike,
|
|
String,
|
|
None,
|
|
}
|
|
|
|
function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined {
|
|
// For a union, return the first one with a recommended completion.
|
|
return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => {
|
|
const symbol = type && type.symbol;
|
|
// Don't include make a recommended completion for an abstract class
|
|
return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol))
|
|
? getFirstSymbolInChain(symbol, previousToken, checker)
|
|
: undefined;
|
|
});
|
|
}
|
|
|
|
function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
|
|
const { parent } = previousToken;
|
|
switch (previousToken.kind) {
|
|
case SyntaxKind.Identifier:
|
|
return getContextualTypeFromParent(previousToken as Identifier, checker);
|
|
case SyntaxKind.EqualsToken:
|
|
switch (parent.kind) {
|
|
case SyntaxKind.VariableDeclaration:
|
|
return checker.getContextualType((parent as VariableDeclaration).initializer!); // TODO: GH#18217
|
|
case SyntaxKind.BinaryExpression:
|
|
return checker.getTypeAtLocation((parent as BinaryExpression).left);
|
|
case SyntaxKind.JsxAttribute:
|
|
return checker.getContextualTypeForJsxAttribute(parent as JsxAttribute);
|
|
default:
|
|
return undefined;
|
|
}
|
|
case SyntaxKind.NewKeyword:
|
|
return checker.getContextualType(parent as Expression);
|
|
case SyntaxKind.CaseKeyword:
|
|
return getSwitchedType(cast(parent, isCaseClause), checker);
|
|
case SyntaxKind.OpenBraceToken:
|
|
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
|
|
default:
|
|
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile);
|
|
return argInfo
|
|
// At `,`, treat this as the next argument after the comma.
|
|
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0))
|
|
: isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
|
|
// completion at `x ===/**/` should be for the right side
|
|
? checker.getTypeAtLocation(parent.left)
|
|
: checker.getContextualType(previousToken as Expression);
|
|
}
|
|
}
|
|
|
|
function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined {
|
|
const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false);
|
|
if (chain) return first(chain);
|
|
return symbol.parent && (isModuleSymbol(symbol.parent) ? symbol : getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker));
|
|
}
|
|
|
|
function isModuleSymbol(symbol: Symbol): boolean {
|
|
return symbol.declarations.some(d => d.kind === SyntaxKind.SourceFile);
|
|
}
|
|
|
|
function getCompletionData(
|
|
program: Program,
|
|
log: (message: string) => void,
|
|
sourceFile: SourceFile,
|
|
isUncheckedFile: boolean,
|
|
position: number,
|
|
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
|
|
detailsEntryId: CompletionEntryIdentifier | undefined,
|
|
): CompletionData | Request | undefined {
|
|
const typeChecker = program.getTypeChecker();
|
|
|
|
let start = timestamp();
|
|
let currentToken = getTokenAtPosition(sourceFile, position); // TODO: GH#15853
|
|
// We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.)
|
|
|
|
log("getCompletionData: Get current token: " + (timestamp() - start));
|
|
|
|
start = timestamp();
|
|
const insideComment = isInComment(sourceFile, position, currentToken);
|
|
log("getCompletionData: Is inside comment: " + (timestamp() - start));
|
|
|
|
let insideJsDocTagTypeExpression = false;
|
|
let isInSnippetScope = false;
|
|
if (insideComment) {
|
|
if (hasDocComment(sourceFile, position)) {
|
|
if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
|
|
// The current position is next to the '@' sign, when no tag name being provided yet.
|
|
// Provide a full list of tag names
|
|
return { kind: CompletionDataKind.JsDocTagName };
|
|
}
|
|
else {
|
|
// When completion is requested without "@", we will have check to make sure that
|
|
// there are no comments prefix the request position. We will only allow "*" and space.
|
|
// e.g
|
|
// /** |c| /*
|
|
//
|
|
// /**
|
|
// |c|
|
|
// */
|
|
//
|
|
// /**
|
|
// * |c|
|
|
// */
|
|
//
|
|
// /**
|
|
// * |c|
|
|
// */
|
|
const lineStart = getLineStartPositionForPosition(position, sourceFile);
|
|
if (!(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/))) {
|
|
return { kind: CompletionDataKind.JsDocTag };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Completion should work inside certain JsDoc tags. For example:
|
|
// /** @type {number | string} */
|
|
// Completion should work in the brackets
|
|
const tag = getJsDocTagAtPosition(currentToken, position);
|
|
if (tag) {
|
|
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
|
|
return { kind: CompletionDataKind.JsDocTagName };
|
|
}
|
|
if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
|
|
currentToken = getTokenAtPosition(sourceFile, position);
|
|
if (!currentToken ||
|
|
(!isDeclarationName(currentToken) &&
|
|
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
|
|
(<JSDocPropertyTag>currentToken.parent).name !== currentToken))) {
|
|
// Use as type location if inside tag's type expression
|
|
insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression);
|
|
}
|
|
}
|
|
if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
|
|
return { kind: CompletionDataKind.JsDocParameterName, tag };
|
|
}
|
|
}
|
|
|
|
if (!insideJsDocTagTypeExpression) {
|
|
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
|
|
// comment or the plain text part of a jsDoc comment, so no completion should be available
|
|
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
start = timestamp();
|
|
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217
|
|
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
|
|
|
|
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
|
|
// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
|
|
let contextToken = previousToken;
|
|
|
|
// Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS|
|
|
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
|
|
if (contextToken && position <= contextToken.end && (isIdentifier(contextToken) || isKeyword(contextToken.kind))) {
|
|
const start = timestamp();
|
|
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217
|
|
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
|
|
}
|
|
|
|
// Find the node where completion is requested on.
|
|
// Also determine whether we are trying to complete with members of that node
|
|
// or attributes of a JSX tag.
|
|
let node = currentToken;
|
|
let propertyAccessToConvert: PropertyAccessExpression | undefined;
|
|
let isRightOfDot = false;
|
|
let isRightOfOpenTag = false;
|
|
let isStartingCloseTag = false;
|
|
let isJsxInitializer: IsJsxInitializer = false;
|
|
|
|
let location = getTouchingPropertyName(sourceFile, position);
|
|
if (contextToken) {
|
|
// Bail out if this is a known invalid completion location
|
|
if (isCompletionListBlocker(contextToken)) {
|
|
log("Returning an empty list because completion was requested in an invalid position.");
|
|
return undefined;
|
|
}
|
|
|
|
let parent = contextToken.parent;
|
|
if (contextToken.kind === SyntaxKind.DotToken) {
|
|
isRightOfDot = true;
|
|
switch (parent.kind) {
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
propertyAccessToConvert = parent as PropertyAccessExpression;
|
|
node = propertyAccessToConvert.expression;
|
|
break;
|
|
case SyntaxKind.QualifiedName:
|
|
node = (parent as QualifiedName).left;
|
|
break;
|
|
case SyntaxKind.ModuleDeclaration:
|
|
node = (parent as ModuleDeclaration).name;
|
|
break;
|
|
case SyntaxKind.ImportType:
|
|
case SyntaxKind.MetaProperty:
|
|
node = parent;
|
|
break;
|
|
default:
|
|
// There is nothing that precedes the dot, so this likely just a stray character
|
|
// or leading into a '...' token. Just bail out instead.
|
|
return undefined;
|
|
}
|
|
}
|
|
else if (sourceFile.languageVariant === LanguageVariant.JSX) {
|
|
// <UI.Test /* completion position */ />
|
|
// If the tagname is a property access expression, we will then walk up to the top most of property access expression.
|
|
// Then, try to get a JSX container and its associated attributes type.
|
|
if (parent && parent.kind === SyntaxKind.PropertyAccessExpression) {
|
|
contextToken = parent;
|
|
parent = parent.parent;
|
|
}
|
|
|
|
// Fix location
|
|
if (currentToken.parent === location) {
|
|
switch (currentToken.kind) {
|
|
case SyntaxKind.GreaterThanToken:
|
|
if (currentToken.parent.kind === SyntaxKind.JsxElement || currentToken.parent.kind === SyntaxKind.JsxOpeningElement) {
|
|
location = currentToken;
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.SlashToken:
|
|
if (currentToken.parent.kind === SyntaxKind.JsxSelfClosingElement) {
|
|
location = currentToken;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (parent.kind) {
|
|
case SyntaxKind.JsxClosingElement:
|
|
if (contextToken.kind === SyntaxKind.SlashToken) {
|
|
isStartingCloseTag = true;
|
|
location = contextToken;
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.BinaryExpression:
|
|
if (!binaryExpressionMayBeOpenTag(parent as BinaryExpression)) {
|
|
break;
|
|
}
|
|
// falls through
|
|
|
|
case SyntaxKind.JsxSelfClosingElement:
|
|
case SyntaxKind.JsxElement:
|
|
case SyntaxKind.JsxOpeningElement:
|
|
if (contextToken.kind === SyntaxKind.LessThanToken) {
|
|
isRightOfOpenTag = true;
|
|
location = contextToken;
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.JsxAttribute:
|
|
switch (previousToken.kind) {
|
|
case SyntaxKind.EqualsToken:
|
|
isJsxInitializer = true;
|
|
break;
|
|
case SyntaxKind.Identifier:
|
|
// For `<div x=[|f/**/|]`, `parent` will be `x` and `previousToken.parent` will be `f` (which is its own JsxAttribute)
|
|
// Note for `<div someBool f>` we don't want to treat this as a jsx inializer, instead it's the attribute name.
|
|
if (parent !== previousToken.parent &&
|
|
!(parent as JsxAttribute).initializer &&
|
|
findChildOfKind(parent, SyntaxKind.EqualsToken, sourceFile)) {
|
|
isJsxInitializer = previousToken as Identifier;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const semanticStart = timestamp();
|
|
let completionKind = CompletionKind.None;
|
|
let isNewIdentifierLocation = false;
|
|
let keywordFilters = KeywordCompletionFilters.None;
|
|
let symbols: Symbol[] = [];
|
|
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
|
|
|
|
if (isRightOfDot) {
|
|
getTypeScriptMemberSymbols();
|
|
}
|
|
else if (isRightOfOpenTag) {
|
|
const tagSymbols = Debug.assertEachDefined(typeChecker.getJsxIntrinsicTagNamesAt(location), "getJsxIntrinsicTagNames() should all be defined");
|
|
tryGetGlobalSymbols();
|
|
symbols = tagSymbols.concat(symbols);
|
|
completionKind = CompletionKind.MemberLike;
|
|
keywordFilters = KeywordCompletionFilters.None;
|
|
}
|
|
else if (isStartingCloseTag) {
|
|
const tagName = (<JsxElement>contextToken.parent.parent).openingElement.tagName;
|
|
const tagSymbol = typeChecker.getSymbolAtLocation(tagName);
|
|
if (tagSymbol) {
|
|
symbols = [tagSymbol];
|
|
}
|
|
completionKind = CompletionKind.MemberLike;
|
|
keywordFilters = KeywordCompletionFilters.None;
|
|
}
|
|
else {
|
|
// For JavaScript or TypeScript, if we're not after a dot, then just try to get the
|
|
// global symbols in scope. These results should be valid for either language as
|
|
// the set of symbols that can be referenced from this location.
|
|
if (!tryGetGlobalSymbols()) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
|
|
|
|
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
|
|
const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined);
|
|
|
|
const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker);
|
|
return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
|
|
|
|
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
|
|
|
|
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
|
|
switch (tag.kind) {
|
|
case SyntaxKind.JSDocParameterTag:
|
|
case SyntaxKind.JSDocPropertyTag:
|
|
case SyntaxKind.JSDocReturnTag:
|
|
case SyntaxKind.JSDocTypeTag:
|
|
case SyntaxKind.JSDocTypedefTag:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getTypeScriptMemberSymbols(): void {
|
|
// Right of dot member completion list
|
|
completionKind = CompletionKind.PropertyAccess;
|
|
|
|
// Since this is qualified name check its a type node location
|
|
const isImportType = isLiteralImportTypeNode(node);
|
|
const isTypeLocation = insideJsDocTagTypeExpression || (isImportType && !(node as ImportTypeNode).isTypeOf) || isPartOfTypeNode(node.parent);
|
|
const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node);
|
|
const allowTypeOrValue = isRhsOfImportDeclaration || (!isTypeLocation && isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker));
|
|
if (isEntityName(node) || isImportType) {
|
|
const isNamespaceName = isModuleDeclaration(node.parent);
|
|
if (isNamespaceName) isNewIdentifierLocation = true;
|
|
let symbol = typeChecker.getSymbolAtLocation(node);
|
|
if (symbol) {
|
|
symbol = skipAlias(symbol, typeChecker);
|
|
|
|
if (symbol.flags & (SymbolFlags.Module | SymbolFlags.Enum)) {
|
|
// Extract module or enum members
|
|
const exportedSymbols = Debug.assertEachDefined(typeChecker.getExportsOfModule(symbol), "getExportsOfModule() should all be defined");
|
|
const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(isImportType ? <ImportTypeNode>node : <PropertyAccessExpression>(node.parent), symbol.name);
|
|
const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol);
|
|
const isValidAccess: (symbol: Symbol) => boolean =
|
|
isNamespaceName
|
|
// At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion.
|
|
? symbol => !!(symbol.flags & SymbolFlags.Namespace) && !symbol.declarations.every(d => d.parent === node.parent)
|
|
: allowTypeOrValue ?
|
|
// Any kind is allowed when dotting off namespace in internal import equals declaration
|
|
symbol => isValidTypeAccess(symbol) || isValidValueAccess(symbol) :
|
|
isTypeLocation ? isValidTypeAccess : isValidValueAccess;
|
|
for (const exportedSymbol of exportedSymbols) {
|
|
if (isValidAccess(exportedSymbol)) {
|
|
symbols.push(exportedSymbol);
|
|
}
|
|
}
|
|
|
|
// If the module is merged with a value, we must get the type of the class and add its propertes (for inherited static methods).
|
|
if (!isTypeLocation && symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) {
|
|
addTypeProperties(typeChecker.getTypeOfSymbolAtLocation(symbol, node));
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isMetaProperty(node) && (node.keywordToken === SyntaxKind.NewKeyword || node.keywordToken === SyntaxKind.ImportKeyword)) {
|
|
const completion = (node.keywordToken === SyntaxKind.NewKeyword) ? "target" : "meta";
|
|
symbols.push(typeChecker.createSymbol(SymbolFlags.Property, escapeLeadingUnderscores(completion)));
|
|
return;
|
|
}
|
|
|
|
if (!isTypeLocation) {
|
|
addTypeProperties(typeChecker.getTypeAtLocation(node));
|
|
}
|
|
}
|
|
|
|
function addTypeProperties(type: Type): void {
|
|
isNewIdentifierLocation = !!type.getStringIndexType();
|
|
|
|
if (isUncheckedFile) {
|
|
// In javascript files, for union types, we don't just get the members that
|
|
// the individual types have in common, we also include all the members that
|
|
// each individual type has. This is because we're going to add all identifiers
|
|
// anyways. So we might as well elevate the members that were at least part
|
|
// of the individual types to a higher status since we know what they are.
|
|
symbols.push(...getPropertiesForCompletion(type, typeChecker));
|
|
}
|
|
else {
|
|
for (const symbol of type.getApparentProperties()) {
|
|
if (typeChecker.isValidPropertyAccessForCompletions(node.kind === SyntaxKind.ImportType ? <ImportTypeNode>node : <PropertyAccessExpression>node.parent, type, symbol)) {
|
|
addPropertySymbol(symbol);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function addPropertySymbol(symbol: Symbol) {
|
|
// For a computed property with an accessible name like `Symbol.iterator`,
|
|
// we'll add a completion for the *name* `Symbol` instead of for the property.
|
|
// If this is e.g. [Symbol.iterator], add a completion for `Symbol`.
|
|
const computedPropertyName = firstDefined(symbol.declarations, decl => tryCast(getNameOfDeclaration(decl), isComputedPropertyName));
|
|
if (computedPropertyName) {
|
|
const leftMostName = getLeftMostName(computedPropertyName.expression); // The completion is for `Symbol`, not `iterator`.
|
|
const nameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName);
|
|
// If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`.
|
|
const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker);
|
|
if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) {
|
|
symbols.push(firstAccessibleSymbol);
|
|
const moduleSymbol = firstAccessibleSymbol.parent;
|
|
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] =
|
|
!moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false };
|
|
}
|
|
}
|
|
else {
|
|
symbols.push(symbol);
|
|
}
|
|
}
|
|
|
|
/** Given 'a.b.c', returns 'a'. */
|
|
function getLeftMostName(e: Expression): Identifier | undefined {
|
|
return isIdentifier(e) ? e : isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined;
|
|
}
|
|
|
|
function tryGetGlobalSymbols(): boolean {
|
|
const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols()
|
|
|| tryGetImportOrExportClauseCompletionSymbols()
|
|
|| tryGetConstructorCompletion()
|
|
|| tryGetClassLikeCompletionSymbols()
|
|
|| tryGetJsxCompletionSymbols()
|
|
|| (getGlobalCompletions(), GlobalsSearch.Success);
|
|
return result === GlobalsSearch.Success;
|
|
}
|
|
|
|
function tryGetConstructorCompletion(): GlobalsSearch {
|
|
if (!tryGetConstructorLikeCompletionContainer(contextToken)) return GlobalsSearch.Continue;
|
|
// no members, only keywords
|
|
completionKind = CompletionKind.None;
|
|
// Declaring new property/method/accessor
|
|
isNewIdentifierLocation = true;
|
|
// Has keywords for constructor parameter
|
|
keywordFilters = KeywordCompletionFilters.ConstructorParameterKeywords;
|
|
return GlobalsSearch.Success;
|
|
}
|
|
|
|
function tryGetJsxCompletionSymbols(): GlobalsSearch {
|
|
const jsxContainer = tryGetContainingJsxElement(contextToken);
|
|
// Cursor is inside a JSX self-closing element or opening element
|
|
const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes);
|
|
if (!attrsType) return GlobalsSearch.Continue;
|
|
symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties);
|
|
completionKind = CompletionKind.MemberLike;
|
|
isNewIdentifierLocation = false;
|
|
return GlobalsSearch.Success;
|
|
}
|
|
|
|
function getGlobalCompletions(): void {
|
|
keywordFilters = tryGetFunctionLikeBodyCompletionContainer(contextToken) ? KeywordCompletionFilters.FunctionLikeBodyKeywords : KeywordCompletionFilters.All;
|
|
|
|
// Get all entities in the current scope.
|
|
completionKind = CompletionKind.Global;
|
|
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken);
|
|
|
|
if (previousToken !== contextToken) {
|
|
Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'.");
|
|
}
|
|
// We need to find the node that will give us an appropriate scope to begin
|
|
// aggregating completion candidates. This is achieved in 'getScopeNode'
|
|
// by finding the first node that encompasses a position, accounting for whether a node
|
|
// is "complete" to decide whether a position belongs to the node.
|
|
//
|
|
// However, at the end of an identifier, we are interested in the scope of the identifier
|
|
// itself, but fall outside of the identifier. For instance:
|
|
//
|
|
// xyz => x$
|
|
//
|
|
// the cursor is outside of both the 'x' and the arrow function 'xyz => x',
|
|
// so 'xyz' is not returned in our results.
|
|
//
|
|
// We define 'adjustedPosition' so that we may appropriately account for
|
|
// being at the end of an identifier. The intention is that if requesting completion
|
|
// at the end of an identifier, it should be effectively equivalent to requesting completion
|
|
// anywhere inside/at the beginning of the identifier. So in the previous case, the
|
|
// 'adjustedPosition' will work as if requesting completion in the following:
|
|
//
|
|
// xyz => $x
|
|
//
|
|
// If previousToken !== contextToken, then
|
|
// - 'contextToken' was adjusted to the token prior to 'previousToken'
|
|
// because we were at the end of an identifier.
|
|
// - 'previousToken' is defined.
|
|
const adjustedPosition = previousToken !== contextToken ?
|
|
previousToken.getStart() :
|
|
position;
|
|
|
|
const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile;
|
|
isInSnippetScope = isSnippetScope(scopeNode);
|
|
|
|
const isTypeOnly = isTypeOnlyCompletion();
|
|
const symbolMeanings = (isTypeOnly ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias;
|
|
|
|
symbols = Debug.assertEachDefined(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings), "getSymbolsInScope() should all be defined");
|
|
|
|
// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
|
|
if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) {
|
|
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
|
|
if (thisType) {
|
|
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
|
|
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType };
|
|
symbols.push(symbol);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldOfferImportCompletions()) {
|
|
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target!);
|
|
}
|
|
filterGlobalCompletion(symbols);
|
|
}
|
|
|
|
function shouldOfferImportCompletions(): boolean {
|
|
// If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs)
|
|
if (!preferences.includeCompletionsForModuleExports) return false;
|
|
// If already using ES6 modules, OK to continue using them.
|
|
if (sourceFile.externalModuleIndicator) return true;
|
|
// If already using commonjs, don't introduce ES6.
|
|
if (sourceFile.commonJsModuleIndicator) return false;
|
|
// If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK.
|
|
if (compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) return true;
|
|
// If some file is using ES6 modules, assume that it's OK to add more.
|
|
return programContainsEs6Modules(program);
|
|
}
|
|
|
|
function isSnippetScope(scopeNode: Node): boolean {
|
|
switch (scopeNode.kind) {
|
|
case SyntaxKind.SourceFile:
|
|
case SyntaxKind.TemplateExpression:
|
|
case SyntaxKind.JsxExpression:
|
|
case SyntaxKind.Block:
|
|
return true;
|
|
default:
|
|
return isStatement(scopeNode);
|
|
}
|
|
}
|
|
|
|
function filterGlobalCompletion(symbols: Symbol[]): void {
|
|
const isTypeOnly = isTypeOnlyCompletion();
|
|
const allowTypes = isTypeOnly || !isContextTokenValueLocation(contextToken) && isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker);
|
|
if (isTypeOnly) keywordFilters = KeywordCompletionFilters.TypeKeywords;
|
|
|
|
filterMutate(symbols, symbol => {
|
|
if (!isSourceFile(location)) {
|
|
// export = /**/ here we want to get all meanings, so any symbol is ok
|
|
if (isExportAssignment(location.parent)) {
|
|
return true;
|
|
}
|
|
|
|
symbol = skipAlias(symbol, typeChecker);
|
|
|
|
// import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace)
|
|
if (isInRightSideOfInternalImportEqualsDeclaration(location)) {
|
|
return !!(symbol.flags & SymbolFlags.Namespace);
|
|
}
|
|
|
|
if (allowTypes) {
|
|
// Its a type, but you can reach it by namespace.type as well
|
|
const symbolAllowedAsType = symbolCanBeReferencedAtTypeLocation(symbol);
|
|
if (symbolAllowedAsType || isTypeOnly) {
|
|
return symbolAllowedAsType;
|
|
}
|
|
}
|
|
}
|
|
|
|
// expressions are value space (which includes the value namespaces)
|
|
return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value);
|
|
});
|
|
}
|
|
|
|
function isTypeOnlyCompletion(): boolean {
|
|
return insideJsDocTagTypeExpression || !isContextTokenValueLocation(contextToken) && (isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken));
|
|
}
|
|
|
|
function isContextTokenValueLocation(contextToken: Node) {
|
|
return contextToken &&
|
|
contextToken.kind === SyntaxKind.TypeOfKeyword &&
|
|
(contextToken.parent.kind === SyntaxKind.TypeQuery || isTypeOfExpression(contextToken.parent));
|
|
}
|
|
|
|
function isContextTokenTypeLocation(contextToken: Node): boolean {
|
|
if (contextToken) {
|
|
const parentKind = contextToken.parent.kind;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.ColonToken:
|
|
return parentKind === SyntaxKind.PropertyDeclaration ||
|
|
parentKind === SyntaxKind.PropertySignature ||
|
|
parentKind === SyntaxKind.Parameter ||
|
|
parentKind === SyntaxKind.VariableDeclaration ||
|
|
isFunctionLikeKind(parentKind);
|
|
|
|
case SyntaxKind.EqualsToken:
|
|
return parentKind === SyntaxKind.TypeAliasDeclaration;
|
|
|
|
case SyntaxKind.AsKeyword:
|
|
return parentKind === SyntaxKind.AsExpression;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** True if symbol is a type or a module containing at least one type. */
|
|
function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, seenModules = createMap<true>()): boolean {
|
|
const sym = skipAlias(symbol.exportSymbol || symbol, typeChecker);
|
|
return !!(sym.flags & SymbolFlags.Type) ||
|
|
!!(sym.flags & SymbolFlags.Module) &&
|
|
addToSeen(seenModules, getSymbolId(sym)) &&
|
|
typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules));
|
|
}
|
|
|
|
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
|
|
const tokenTextLowerCase = tokenText.toLowerCase();
|
|
|
|
const seenResolvedModules = createMap<true>();
|
|
|
|
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
|
|
// Perf -- ignore other modules if this is a request for details
|
|
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
|
|
return;
|
|
}
|
|
|
|
const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol);
|
|
// resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one.
|
|
if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) {
|
|
return;
|
|
}
|
|
|
|
if (resolvedModuleSymbol !== moduleSymbol &&
|
|
// 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`.
|
|
some(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator)) {
|
|
symbols.push(resolvedModuleSymbol);
|
|
symbolToOriginInfoMap[getSymbolId(resolvedModuleSymbol)] = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport: false };
|
|
}
|
|
|
|
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
|
|
// Don't add a completion for a re-export, only for the original.
|
|
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
|
|
// This is just to avoid adding duplicate completion entries.
|
|
//
|
|
// If `symbol.parent !== ...`, this is an `export * from "foo"` re-export. Those don't create new symbols.
|
|
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol
|
|
|| some(symbol.declarations, d =>
|
|
// If `!!d.name.originalKeywordKind`, this is `export { _break as break };` -- skip this and prefer the keyword completion.
|
|
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
|
|
isExportSpecifier(d) && (d.propertyName ? isIdentifierANonContextualKeyword(d.name) : !!d.parent.parent.moduleSpecifier))) {
|
|
continue;
|
|
}
|
|
|
|
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
|
|
if (isDefaultExport) {
|
|
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
|
|
}
|
|
|
|
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
|
|
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
|
|
symbols.push(symbol);
|
|
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* True if you could remove some characters in `a` to get `b`.
|
|
* E.g., true for "abcdef" and "bdf".
|
|
* But not true for "abcdef" and "dbf".
|
|
*/
|
|
function stringContainsCharactersInOrder(str: string, characters: string): boolean {
|
|
if (characters.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
let characterIndex = 0;
|
|
for (let strIndex = 0; strIndex < str.length; strIndex++) {
|
|
if (str.charCodeAt(strIndex) === characters.charCodeAt(characterIndex)) {
|
|
characterIndex++;
|
|
if (characterIndex === characters.length) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Did not find all characters
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Finds the first node that "embraces" the position, so that one may
|
|
* accurately aggregate locals from the closest containing scope.
|
|
*/
|
|
function getScopeNode(initialToken: Node | undefined, position: number, sourceFile: SourceFile) {
|
|
let scope: Node | undefined = initialToken;
|
|
while (scope && !positionBelongsToNode(scope, position, sourceFile)) {
|
|
scope = scope.parent;
|
|
}
|
|
return scope;
|
|
}
|
|
|
|
function isCompletionListBlocker(contextToken: Node): boolean {
|
|
const start = timestamp();
|
|
const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) ||
|
|
isSolelyIdentifierDefinitionLocation(contextToken) ||
|
|
isDotOfNumericLiteral(contextToken) ||
|
|
isInJsxText(contextToken);
|
|
log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start));
|
|
return result;
|
|
}
|
|
|
|
function isInJsxText(contextToken: Node): boolean {
|
|
if (contextToken.kind === SyntaxKind.JsxText) {
|
|
return true;
|
|
}
|
|
|
|
if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) {
|
|
if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) {
|
|
return true;
|
|
}
|
|
|
|
if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) {
|
|
return !!contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isNewIdentifierDefinitionLocation(previousToken: Node | undefined): boolean {
|
|
if (previousToken) {
|
|
const containingNodeKind = previousToken.parent.kind;
|
|
// Previous token may have been a keyword that was converted to an identifier.
|
|
switch (keywordForNode(previousToken)) {
|
|
case SyntaxKind.CommaToken:
|
|
return containingNodeKind === SyntaxKind.CallExpression // func( a, |
|
|
|| containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */
|
|
|| containingNodeKind === SyntaxKind.NewExpression // new C(a, |
|
|
|| containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, |
|
|
|| containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, |
|
|
|| containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list|
|
|
|
|
case SyntaxKind.OpenParenToken:
|
|
return containingNodeKind === SyntaxKind.CallExpression // func( |
|
|
|| containingNodeKind === SyntaxKind.Constructor // constructor( |
|
|
|| containingNodeKind === SyntaxKind.NewExpression // new C(a|
|
|
|| containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a|
|
|
|| containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */
|
|
|
|
case SyntaxKind.OpenBracketToken:
|
|
return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ |
|
|
|| containingNodeKind === SyntaxKind.IndexSignature // [ | : string ]
|
|
|| containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */
|
|
|
|
case SyntaxKind.ModuleKeyword: // module |
|
|
case SyntaxKind.NamespaceKeyword: // namespace |
|
|
return true;
|
|
|
|
case SyntaxKind.DotToken:
|
|
return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.|
|
|
|
|
case SyntaxKind.OpenBraceToken:
|
|
return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ |
|
|
|
|
case SyntaxKind.EqualsToken:
|
|
return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a|
|
|
|| containingNodeKind === SyntaxKind.BinaryExpression; // x = a|
|
|
|
|
case SyntaxKind.TemplateHead:
|
|
return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${|
|
|
|
|
case SyntaxKind.TemplateMiddle:
|
|
return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${|
|
|
|
|
case SyntaxKind.PublicKeyword:
|
|
case SyntaxKind.PrivateKeyword:
|
|
case SyntaxKind.ProtectedKeyword:
|
|
return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public |
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean {
|
|
// To be "in" one of these literals, the position has to be:
|
|
// 1. entirely within the token text.
|
|
// 2. at the end position of an unterminated token.
|
|
// 3. at the end of a regular expression (due to trailing flags like '/foo/g').
|
|
return (isRegularExpressionLiteral(contextToken) || isStringTextContainingNode(contextToken)) && (
|
|
rangeContainsPositionExclusive(createTextRangeFromSpan(createTextSpanFromNode(contextToken)), position) ||
|
|
position === contextToken.end && (!!contextToken.isUnterminated || isRegularExpressionLiteral(contextToken)));
|
|
}
|
|
|
|
/**
|
|
* Aggregates relevant symbols for completion in object literals and object binding patterns.
|
|
* Relevant symbols are stored in the captured 'symbols' variable.
|
|
*
|
|
* @returns true if 'symbols' was successfully populated; false otherwise.
|
|
*/
|
|
function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined {
|
|
const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken);
|
|
if (!objectLikeContainer) return GlobalsSearch.Continue;
|
|
|
|
// We're looking up possible property names from contextual/inferred/declared type.
|
|
completionKind = CompletionKind.ObjectPropertyDeclaration;
|
|
|
|
let typeMembers: Symbol[] | undefined;
|
|
let existingMembers: ReadonlyArray<Declaration> | undefined;
|
|
|
|
if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) {
|
|
const typeForObject = typeChecker.getContextualType(objectLikeContainer);
|
|
if (!typeForObject) return GlobalsSearch.Fail;
|
|
isNewIdentifierLocation = hasIndexSignature(typeForObject);
|
|
typeMembers = getPropertiesForObjectExpression(typeForObject, objectLikeContainer, typeChecker);
|
|
existingMembers = objectLikeContainer.properties;
|
|
}
|
|
else {
|
|
Debug.assert(objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern);
|
|
// We are *only* completing on properties from the type being destructured.
|
|
isNewIdentifierLocation = false;
|
|
|
|
const rootDeclaration = getRootDeclaration(objectLikeContainer.parent);
|
|
if (!isVariableLike(rootDeclaration)) return Debug.fail("Root declaration is not variable-like.");
|
|
|
|
// We don't want to complete using the type acquired by the shape
|
|
// of the binding pattern; we are only interested in types acquired
|
|
// through type declaration or inference.
|
|
// Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed -
|
|
// type of parameter will flow in from the contextual type of the function
|
|
let canGetType = hasInitializer(rootDeclaration) || hasType(rootDeclaration) || rootDeclaration.parent.parent.kind === SyntaxKind.ForOfStatement;
|
|
if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) {
|
|
if (isExpression(rootDeclaration.parent)) {
|
|
canGetType = !!typeChecker.getContextualType(<Expression>rootDeclaration.parent);
|
|
}
|
|
else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) {
|
|
canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(<Expression>rootDeclaration.parent.parent);
|
|
}
|
|
}
|
|
if (canGetType) {
|
|
const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer);
|
|
if (!typeForObject) return GlobalsSearch.Fail;
|
|
// In a binding pattern, get only known properties. Everywhere else we will get all possible properties.
|
|
typeMembers = typeChecker.getPropertiesOfType(typeForObject).filter((symbol) => !(getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.NonPublicAccessibilityModifier));
|
|
existingMembers = objectLikeContainer.elements;
|
|
}
|
|
}
|
|
|
|
if (typeMembers && typeMembers.length > 0) {
|
|
// Add filtered items to the completion list
|
|
symbols = filterObjectMembersList(typeMembers, Debug.assertDefined(existingMembers));
|
|
}
|
|
return GlobalsSearch.Success;
|
|
}
|
|
|
|
/**
|
|
* Aggregates relevant symbols for completion in import clauses and export clauses
|
|
* whose declarations have a module specifier; for instance, symbols will be aggregated for
|
|
*
|
|
* import { | } from "moduleName";
|
|
* export { a as foo, | } from "moduleName";
|
|
*
|
|
* but not for
|
|
*
|
|
* export { | };
|
|
*
|
|
* Relevant symbols are stored in the captured 'symbols' variable.
|
|
*
|
|
* @returns true if 'symbols' was successfully populated; false otherwise.
|
|
*/
|
|
function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch {
|
|
// `import { |` or `import { a as 0, | }`
|
|
const namedImportsOrExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken)
|
|
? tryCast(contextToken.parent, isNamedImportsOrExports) : undefined;
|
|
if (!namedImportsOrExports) return GlobalsSearch.Continue;
|
|
|
|
// cursor is in an import clause
|
|
// try to show exported member for imported module
|
|
const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent;
|
|
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier!); // TODO: GH#18217
|
|
if (!moduleSpecifierSymbol) return GlobalsSearch.Fail;
|
|
|
|
completionKind = CompletionKind.MemberLike;
|
|
isNewIdentifierLocation = false;
|
|
const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol);
|
|
const existing = arrayToSet<ImportOrExportSpecifier>(namedImportsOrExports.elements, n => isCurrentlyEditingNode(n) ? undefined : (n.propertyName || n.name).escapedText);
|
|
symbols = exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.get(e.escapedName));
|
|
return GlobalsSearch.Success;
|
|
}
|
|
|
|
/**
|
|
* Aggregates relevant symbols for completion in class declaration
|
|
* Relevant symbols are stored in the captured 'symbols' variable.
|
|
*/
|
|
function tryGetClassLikeCompletionSymbols(): GlobalsSearch {
|
|
const decl = tryGetObjectTypeDeclarationCompletionContainer(sourceFile, contextToken, location);
|
|
if (!decl) return GlobalsSearch.Continue;
|
|
|
|
// We're looking up possible property names from parent type.
|
|
completionKind = CompletionKind.MemberLike;
|
|
// Declaring new property/method/accessor
|
|
isNewIdentifierLocation = true;
|
|
keywordFilters = contextToken.kind === SyntaxKind.AsteriskToken ? KeywordCompletionFilters.None :
|
|
isClassLike(decl) ? KeywordCompletionFilters.ClassElementKeywords : KeywordCompletionFilters.InterfaceElementKeywords;
|
|
|
|
// If you're in an interface you don't want to repeat things from super-interface. So just stop here.
|
|
if (!isClassLike(decl)) return GlobalsSearch.Success;
|
|
|
|
const classElement = contextToken.parent;
|
|
let classElementModifierFlags = isClassElement(classElement) ? getModifierFlags(classElement) : ModifierFlags.None;
|
|
// If this is context token is not something we are editing now, consider if this would lead to be modifier
|
|
if (contextToken.kind === SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) {
|
|
switch (contextToken.getText()) {
|
|
case "private":
|
|
classElementModifierFlags = classElementModifierFlags | ModifierFlags.Private;
|
|
break;
|
|
case "static":
|
|
classElementModifierFlags = classElementModifierFlags | ModifierFlags.Static;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No member list for private methods
|
|
if (!(classElementModifierFlags & ModifierFlags.Private)) {
|
|
// List of property symbols of base type that are not private and already implemented
|
|
const baseSymbols = flatMap(getAllSuperTypeNodes(decl), baseTypeNode => {
|
|
const type = typeChecker.getTypeAtLocation(baseTypeNode);
|
|
return type && typeChecker.getPropertiesOfType(classElementModifierFlags & ModifierFlags.Static ? typeChecker.getTypeOfSymbolAtLocation(type.symbol, decl) : type);
|
|
});
|
|
symbols = filterClassMembersList(baseSymbols, decl.members, classElementModifierFlags);
|
|
}
|
|
|
|
return GlobalsSearch.Success;
|
|
}
|
|
|
|
/**
|
|
* Returns the immediate owning object literal or binding pattern of a context token,
|
|
* on the condition that one exists and that the context implies completion should be given.
|
|
*/
|
|
function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | ObjectBindingPattern | undefined {
|
|
if (contextToken) {
|
|
const { parent } = contextToken;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.OpenBraceToken: // const x = { |
|
|
case SyntaxKind.CommaToken: // const x = { a: 0, |
|
|
if (isObjectLiteralExpression(parent) || isObjectBindingPattern(parent)) {
|
|
return parent;
|
|
}
|
|
break;
|
|
case SyntaxKind.AsteriskToken:
|
|
return isMethodDeclaration(parent) ? tryCast(parent.parent, isObjectLiteralExpression) : undefined;
|
|
case SyntaxKind.Identifier:
|
|
return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent)
|
|
? contextToken.parent.parent : undefined;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function isConstructorParameterCompletion(node: Node): boolean {
|
|
return !!node.parent && isParameter(node.parent) && isConstructorDeclaration(node.parent.parent)
|
|
&& (isParameterPropertyModifier(node.kind) || isDeclarationName(node));
|
|
}
|
|
|
|
/**
|
|
* Returns the immediate owning class declaration of a context token,
|
|
* on the condition that one exists and that the context implies completion should be given.
|
|
*/
|
|
function tryGetConstructorLikeCompletionContainer(contextToken: Node): ConstructorDeclaration | undefined {
|
|
if (contextToken) {
|
|
const parent = contextToken.parent;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.OpenParenToken:
|
|
case SyntaxKind.CommaToken:
|
|
return isConstructorDeclaration(contextToken.parent) ? contextToken.parent : undefined;
|
|
|
|
default:
|
|
if (isConstructorParameterCompletion(contextToken)) {
|
|
return parent.parent as ConstructorDeclaration;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function tryGetFunctionLikeBodyCompletionContainer(contextToken: Node): FunctionLikeDeclaration | undefined {
|
|
if (contextToken) {
|
|
let prev: Node;
|
|
const container = findAncestor(contextToken.parent, (node: Node) => {
|
|
if (isClassLike(node)) {
|
|
return "quit";
|
|
}
|
|
if (isFunctionLikeDeclaration(node) && prev === node.body) {
|
|
return true;
|
|
}
|
|
prev = node;
|
|
return false;
|
|
});
|
|
return container && container as FunctionLikeDeclaration;
|
|
}
|
|
}
|
|
|
|
function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement | undefined {
|
|
if (contextToken) {
|
|
const parent = contextToken.parent;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.GreaterThanToken: // End of a type argument list
|
|
case SyntaxKind.LessThanSlashToken:
|
|
case SyntaxKind.SlashToken:
|
|
case SyntaxKind.Identifier:
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
case SyntaxKind.JsxAttributes:
|
|
case SyntaxKind.JsxAttribute:
|
|
case SyntaxKind.JsxSpreadAttribute:
|
|
if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) {
|
|
if (contextToken.kind === SyntaxKind.GreaterThanToken) {
|
|
const precedingToken = findPrecedingToken(contextToken.pos, sourceFile, /*startNode*/ undefined);
|
|
if (!(parent as JsxOpeningLikeElement).typeArguments || (precedingToken && precedingToken.kind === SyntaxKind.SlashToken)) break;
|
|
}
|
|
return <JsxOpeningLikeElement>parent;
|
|
}
|
|
else if (parent.kind === SyntaxKind.JsxAttribute) {
|
|
// Currently we parse JsxOpeningLikeElement as:
|
|
// JsxOpeningLikeElement
|
|
// attributes: JsxAttributes
|
|
// properties: NodeArray<JsxAttributeLike>
|
|
return parent.parent.parent as JsxOpeningLikeElement;
|
|
}
|
|
break;
|
|
|
|
// The context token is the closing } or " of an attribute, which means
|
|
// its parent is a JsxExpression, whose parent is a JsxAttribute,
|
|
// whose parent is a JsxOpeningLikeElement
|
|
case SyntaxKind.StringLiteral:
|
|
if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) {
|
|
// Currently we parse JsxOpeningLikeElement as:
|
|
// JsxOpeningLikeElement
|
|
// attributes: JsxAttributes
|
|
// properties: NodeArray<JsxAttributeLike>
|
|
return parent.parent.parent as JsxOpeningLikeElement;
|
|
}
|
|
|
|
break;
|
|
|
|
case SyntaxKind.CloseBraceToken:
|
|
if (parent &&
|
|
parent.kind === SyntaxKind.JsxExpression &&
|
|
parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) {
|
|
// Currently we parse JsxOpeningLikeElement as:
|
|
// JsxOpeningLikeElement
|
|
// attributes: JsxAttributes
|
|
// properties: NodeArray<JsxAttributeLike>
|
|
// each JsxAttribute can have initializer as JsxExpression
|
|
return parent.parent.parent.parent as JsxOpeningLikeElement;
|
|
}
|
|
|
|
if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) {
|
|
// Currently we parse JsxOpeningLikeElement as:
|
|
// JsxOpeningLikeElement
|
|
// attributes: JsxAttributes
|
|
// properties: NodeArray<JsxAttributeLike>
|
|
return parent.parent.parent as JsxOpeningLikeElement;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @returns true if we are certain that the currently edited location must define a new location; false otherwise.
|
|
*/
|
|
function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean {
|
|
const parent = contextToken.parent;
|
|
const containingNodeKind = parent.kind;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.CommaToken:
|
|
return containingNodeKind === SyntaxKind.VariableDeclaration ||
|
|
containingNodeKind === SyntaxKind.VariableDeclarationList ||
|
|
containingNodeKind === SyntaxKind.VariableStatement ||
|
|
containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, |
|
|
isFunctionLikeButNotConstructor(containingNodeKind) ||
|
|
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A<T, |
|
|
containingNodeKind === SyntaxKind.ArrayBindingPattern || // var [x, y|
|
|
containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type Map, K, |
|
|
// class A<T, |
|
|
// var C = class D<T, |
|
|
(isClassLike(parent) &&
|
|
!!parent.typeParameters &&
|
|
parent.typeParameters.end >= contextToken.pos);
|
|
|
|
case SyntaxKind.DotToken:
|
|
return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [.|
|
|
|
|
case SyntaxKind.ColonToken:
|
|
return containingNodeKind === SyntaxKind.BindingElement; // var {x :html|
|
|
|
|
case SyntaxKind.OpenBracketToken:
|
|
return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [x|
|
|
|
|
case SyntaxKind.OpenParenToken:
|
|
return containingNodeKind === SyntaxKind.CatchClause ||
|
|
isFunctionLikeButNotConstructor(containingNodeKind);
|
|
|
|
case SyntaxKind.OpenBraceToken:
|
|
return containingNodeKind === SyntaxKind.EnumDeclaration; // enum a { |
|
|
|
|
case SyntaxKind.LessThanToken:
|
|
return containingNodeKind === SyntaxKind.ClassDeclaration || // class A< |
|
|
containingNodeKind === SyntaxKind.ClassExpression || // var C = class D< |
|
|
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A< |
|
|
containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type List< |
|
|
isFunctionLikeKind(containingNodeKind);
|
|
|
|
case SyntaxKind.StaticKeyword:
|
|
return containingNodeKind === SyntaxKind.PropertyDeclaration && !isClassLike(parent.parent);
|
|
|
|
case SyntaxKind.DotDotDotToken:
|
|
return containingNodeKind === SyntaxKind.Parameter ||
|
|
(!!parent.parent && parent.parent.kind === SyntaxKind.ArrayBindingPattern); // var [...z|
|
|
|
|
case SyntaxKind.PublicKeyword:
|
|
case SyntaxKind.PrivateKeyword:
|
|
case SyntaxKind.ProtectedKeyword:
|
|
return containingNodeKind === SyntaxKind.Parameter && !isConstructorDeclaration(parent.parent);
|
|
|
|
case SyntaxKind.AsKeyword:
|
|
return containingNodeKind === SyntaxKind.ImportSpecifier ||
|
|
containingNodeKind === SyntaxKind.ExportSpecifier ||
|
|
containingNodeKind === SyntaxKind.NamespaceImport;
|
|
|
|
case SyntaxKind.GetKeyword:
|
|
case SyntaxKind.SetKeyword:
|
|
return !isFromObjectTypeDeclaration(contextToken);
|
|
|
|
case SyntaxKind.ClassKeyword:
|
|
case SyntaxKind.EnumKeyword:
|
|
case SyntaxKind.InterfaceKeyword:
|
|
case SyntaxKind.FunctionKeyword:
|
|
case SyntaxKind.VarKeyword:
|
|
case SyntaxKind.ImportKeyword:
|
|
case SyntaxKind.LetKeyword:
|
|
case SyntaxKind.ConstKeyword:
|
|
case SyntaxKind.YieldKeyword:
|
|
case SyntaxKind.TypeKeyword: // type htm|
|
|
return true;
|
|
|
|
case SyntaxKind.AsteriskToken:
|
|
return isFunctionLike(contextToken.parent) && !isMethodDeclaration(contextToken.parent);
|
|
}
|
|
|
|
// If the previous token is keyword correspoding to class member completion keyword
|
|
// there will be completion available here
|
|
if (isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken)) {
|
|
return false;
|
|
}
|
|
|
|
if (isConstructorParameterCompletion(contextToken)) {
|
|
// constructor parameter completion is available only if
|
|
// - its modifier of the constructor parameter or
|
|
// - its name of the parameter and not being edited
|
|
// eg. constructor(a |<- this shouldnt show completion
|
|
if (!isIdentifier(contextToken) ||
|
|
isParameterPropertyModifier(keywordForNode(contextToken)) ||
|
|
isCurrentlyEditingNode(contextToken)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Previous token may have been a keyword that was converted to an identifier.
|
|
switch (keywordForNode(contextToken)) {
|
|
case SyntaxKind.AbstractKeyword:
|
|
case SyntaxKind.ClassKeyword:
|
|
case SyntaxKind.ConstKeyword:
|
|
case SyntaxKind.DeclareKeyword:
|
|
case SyntaxKind.EnumKeyword:
|
|
case SyntaxKind.FunctionKeyword:
|
|
case SyntaxKind.InterfaceKeyword:
|
|
case SyntaxKind.LetKeyword:
|
|
case SyntaxKind.PrivateKeyword:
|
|
case SyntaxKind.ProtectedKeyword:
|
|
case SyntaxKind.PublicKeyword:
|
|
case SyntaxKind.StaticKeyword:
|
|
case SyntaxKind.VarKeyword:
|
|
case SyntaxKind.YieldKeyword:
|
|
return true;
|
|
case SyntaxKind.AsyncKeyword:
|
|
return isPropertyDeclaration(contextToken.parent);
|
|
}
|
|
|
|
return isDeclarationName(contextToken)
|
|
&& !isJsxAttribute(contextToken.parent)
|
|
// Don't block completions if we're in `class C /**/`, because we're *past* the end of the identifier and might want to complete `extends`.
|
|
// If `contextToken !== previousToken`, this is `class C ex/**/`.
|
|
&& !(isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end));
|
|
}
|
|
|
|
function isFunctionLikeButNotConstructor(kind: SyntaxKind) {
|
|
return isFunctionLikeKind(kind) && kind !== SyntaxKind.Constructor;
|
|
}
|
|
|
|
function isDotOfNumericLiteral(contextToken: Node): boolean {
|
|
if (contextToken.kind === SyntaxKind.NumericLiteral) {
|
|
const text = contextToken.getFullText();
|
|
return text.charAt(text.length - 1) === ".";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions for named imports or exports.
|
|
*
|
|
* @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations
|
|
* do not occur at the current position and have not otherwise been typed.
|
|
*/
|
|
function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: ReadonlyArray<Declaration>): Symbol[] {
|
|
if (existingMembers.length === 0) {
|
|
return contextualMemberSymbols;
|
|
}
|
|
|
|
const existingMemberNames = createUnderscoreEscapedMap<boolean>();
|
|
for (const m of existingMembers) {
|
|
// Ignore omitted expressions for missing members
|
|
if (m.kind !== SyntaxKind.PropertyAssignment &&
|
|
m.kind !== SyntaxKind.ShorthandPropertyAssignment &&
|
|
m.kind !== SyntaxKind.BindingElement &&
|
|
m.kind !== SyntaxKind.MethodDeclaration &&
|
|
m.kind !== SyntaxKind.GetAccessor &&
|
|
m.kind !== SyntaxKind.SetAccessor) {
|
|
continue;
|
|
}
|
|
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (isCurrentlyEditingNode(m)) {
|
|
continue;
|
|
}
|
|
|
|
let existingName: __String | undefined;
|
|
|
|
if (isBindingElement(m) && m.propertyName) {
|
|
// include only identifiers in completion list
|
|
if (m.propertyName.kind === SyntaxKind.Identifier) {
|
|
existingName = m.propertyName.escapedText;
|
|
}
|
|
}
|
|
else {
|
|
// TODO: Account for computed property name
|
|
// NOTE: if one only performs this step when m.name is an identifier,
|
|
// things like '__proto__' are not filtered out.
|
|
const name = getNameOfDeclaration(m);
|
|
existingName = name && isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined;
|
|
}
|
|
|
|
existingMemberNames.set(existingName!, true); // TODO: GH#18217
|
|
}
|
|
|
|
return contextualMemberSymbols.filter(m => !existingMemberNames.get(m.escapedName));
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions for class elements.
|
|
*
|
|
* @returns Symbols to be suggested in an class element depending on existing memebers and symbol flags
|
|
*/
|
|
function filterClassMembersList(baseSymbols: ReadonlyArray<Symbol>, existingMembers: ReadonlyArray<ClassElement>, currentClassElementModifierFlags: ModifierFlags): Symbol[] {
|
|
const existingMemberNames = createUnderscoreEscapedMap<true>();
|
|
for (const m of existingMembers) {
|
|
// Ignore omitted expressions for missing members
|
|
if (m.kind !== SyntaxKind.PropertyDeclaration &&
|
|
m.kind !== SyntaxKind.MethodDeclaration &&
|
|
m.kind !== SyntaxKind.GetAccessor &&
|
|
m.kind !== SyntaxKind.SetAccessor) {
|
|
continue;
|
|
}
|
|
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (isCurrentlyEditingNode(m)) {
|
|
continue;
|
|
}
|
|
|
|
// Dont filter member even if the name matches if it is declared private in the list
|
|
if (hasModifier(m, ModifierFlags.Private)) {
|
|
continue;
|
|
}
|
|
|
|
// do not filter it out if the static presence doesnt match
|
|
if (hasModifier(m, ModifierFlags.Static) !== !!(currentClassElementModifierFlags & ModifierFlags.Static)) {
|
|
continue;
|
|
}
|
|
|
|
const existingName = getPropertyNameForPropertyNameNode(m.name!);
|
|
if (existingName) {
|
|
existingMemberNames.set(existingName, true);
|
|
}
|
|
}
|
|
|
|
return baseSymbols.filter(propertySymbol =>
|
|
!existingMemberNames.has(propertySymbol.escapedName) &&
|
|
!!propertySymbol.declarations &&
|
|
!(getDeclarationModifierFlagsFromSymbol(propertySymbol) & ModifierFlags.Private));
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions from 'symbols' according to existing JSX attributes.
|
|
*
|
|
* @returns Symbols to be suggested in a JSX element, barring those whose attributes
|
|
* do not occur at the current position and have not otherwise been typed.
|
|
*/
|
|
function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>): Symbol[] {
|
|
const seenNames = createUnderscoreEscapedMap<boolean>();
|
|
for (const attr of attributes) {
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (isCurrentlyEditingNode(attr)) {
|
|
continue;
|
|
}
|
|
|
|
if (attr.kind === SyntaxKind.JsxAttribute) {
|
|
seenNames.set(attr.name.escapedText, true);
|
|
}
|
|
}
|
|
|
|
return symbols.filter(a => !seenNames.get(a.escapedName));
|
|
}
|
|
|
|
function isCurrentlyEditingNode(node: Node): boolean {
|
|
return node.getStart(sourceFile) <= position && position <= node.getEnd();
|
|
}
|
|
}
|
|
|
|
interface CompletionEntryDisplayNameForSymbol {
|
|
readonly name: string;
|
|
readonly needsConvertPropertyAccess: boolean;
|
|
}
|
|
function getCompletionEntryDisplayNameForSymbol(
|
|
symbol: Symbol,
|
|
target: ScriptTarget,
|
|
origin: SymbolOriginInfo | undefined,
|
|
kind: CompletionKind,
|
|
): CompletionEntryDisplayNameForSymbol | undefined {
|
|
const name = getSymbolName(symbol, origin, target);
|
|
if (name === undefined
|
|
// If the symbol is external module, don't show it in the completion list
|
|
// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
|
|
|| symbol.flags & SymbolFlags.Module && startsWithQuote(name)
|
|
// If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@"
|
|
|| isKnownSymbol(symbol)) {
|
|
return undefined;
|
|
}
|
|
|
|
const validIdentiferResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false };
|
|
if (isIdentifierText(name, target)) return validIdentiferResult;
|
|
switch (kind) {
|
|
case CompletionKind.MemberLike:
|
|
return undefined;
|
|
case CompletionKind.ObjectPropertyDeclaration:
|
|
// TODO: GH#18169
|
|
return { name: JSON.stringify(name), needsConvertPropertyAccess: false };
|
|
case CompletionKind.PropertyAccess:
|
|
case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name.
|
|
// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547
|
|
return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true };
|
|
case CompletionKind.None:
|
|
case CompletionKind.String:
|
|
return validIdentiferResult;
|
|
default:
|
|
Debug.assertNever(kind);
|
|
}
|
|
}
|
|
|
|
// A cache of completion entries for keywords, these do not change between sessions
|
|
const _keywordCompletions: ReadonlyArray<CompletionEntry>[] = [];
|
|
const allKeywordsCompletions: () => ReadonlyArray<CompletionEntry> = memoize(() => {
|
|
const res: CompletionEntry[] = [];
|
|
for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) {
|
|
res.push({
|
|
name: tokenToString(i)!,
|
|
kind: ScriptElementKind.keyword,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
sortText: "0"
|
|
});
|
|
}
|
|
return res;
|
|
});
|
|
function getKeywordCompletions(keywordFilter: KeywordCompletionFilters): ReadonlyArray<CompletionEntry> {
|
|
return _keywordCompletions[keywordFilter] || (_keywordCompletions[keywordFilter] = allKeywordsCompletions().filter(entry => {
|
|
const kind = stringToToken(entry.name)!;
|
|
switch (keywordFilter) {
|
|
case KeywordCompletionFilters.None:
|
|
return false;
|
|
case KeywordCompletionFilters.All:
|
|
return kind === SyntaxKind.AsyncKeyword || SyntaxKind.AwaitKeyword || !isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind) || kind === SyntaxKind.DeclareKeyword || kind === SyntaxKind.ModuleKeyword
|
|
|| isTypeKeyword(kind) && kind !== SyntaxKind.UndefinedKeyword;
|
|
case KeywordCompletionFilters.ClassElementKeywords:
|
|
return isClassMemberCompletionKeyword(kind);
|
|
case KeywordCompletionFilters.InterfaceElementKeywords:
|
|
return isInterfaceOrTypeLiteralCompletionKeyword(kind);
|
|
case KeywordCompletionFilters.ConstructorParameterKeywords:
|
|
return isParameterPropertyModifier(kind);
|
|
case KeywordCompletionFilters.FunctionLikeBodyKeywords:
|
|
return isFunctionLikeBodyKeyword(kind);
|
|
case KeywordCompletionFilters.TypeKeywords:
|
|
return isTypeKeyword(kind);
|
|
default:
|
|
return Debug.assertNever(keywordFilter);
|
|
}
|
|
}));
|
|
}
|
|
|
|
function isInterfaceOrTypeLiteralCompletionKeyword(kind: SyntaxKind): boolean {
|
|
return kind === SyntaxKind.ReadonlyKeyword;
|
|
}
|
|
|
|
function isClassMemberCompletionKeyword(kind: SyntaxKind) {
|
|
switch (kind) {
|
|
case SyntaxKind.AbstractKeyword:
|
|
case SyntaxKind.ConstructorKeyword:
|
|
case SyntaxKind.GetKeyword:
|
|
case SyntaxKind.SetKeyword:
|
|
case SyntaxKind.AsyncKeyword:
|
|
return true;
|
|
default:
|
|
return isClassMemberModifier(kind);
|
|
}
|
|
}
|
|
|
|
function isFunctionLikeBodyKeyword(kind: SyntaxKind) {
|
|
return kind === SyntaxKind.AsyncKeyword || SyntaxKind.AwaitKeyword || !isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind);
|
|
}
|
|
|
|
function keywordForNode(node: Node): SyntaxKind {
|
|
return isIdentifier(node) ? node.originalKeywordKind || SyntaxKind.Unknown : node.kind;
|
|
}
|
|
|
|
/** Get the corresponding JSDocTag node if the position is in a jsDoc comment */
|
|
function getJsDocTagAtPosition(node: Node, position: number): JSDocTag | undefined {
|
|
const jsdoc = findAncestor(node, isJSDoc);
|
|
return jsdoc && jsdoc.tags && (rangeContainsPosition(jsdoc, position) ? findLast(jsdoc.tags, tag => tag.pos < position) : undefined);
|
|
}
|
|
|
|
function getPropertiesForObjectExpression(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker): Symbol[] {
|
|
return contextualType.isUnion()
|
|
? checker.getAllPossiblePropertiesOfTypes(contextualType.types.filter(memberType =>
|
|
// If we're providing completions for an object literal, skip primitive, array-like, or callable types since those shouldn't be implemented by object literals.
|
|
!(memberType.flags & TypeFlags.Primitive ||
|
|
checker.isArrayLikeType(memberType) ||
|
|
typeHasCallOrConstructSignatures(memberType, checker) ||
|
|
checker.isTypeInvalidDueToUnionDiscriminant(memberType, obj))))
|
|
: contextualType.getApparentProperties();
|
|
}
|
|
|
|
/**
|
|
* Gets all properties on a type, but if that type is a union of several types,
|
|
* excludes array-like types or callable/constructable types.
|
|
*/
|
|
function getPropertiesForCompletion(type: Type, checker: TypeChecker): Symbol[] {
|
|
return type.isUnion()
|
|
? Debug.assertEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined")
|
|
: Debug.assertEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined");
|
|
}
|
|
|
|
/**
|
|
* Returns the immediate owning class declaration of a context token,
|
|
* on the condition that one exists and that the context implies completion should be given.
|
|
*/
|
|
function tryGetObjectTypeDeclarationCompletionContainer(sourceFile: SourceFile, contextToken: Node | undefined, location: Node): ObjectTypeDeclaration | undefined {
|
|
// class c { method() { } | method2() { } }
|
|
switch (location.kind) {
|
|
case SyntaxKind.SyntaxList:
|
|
return tryCast(location.parent, isObjectTypeDeclaration);
|
|
case SyntaxKind.EndOfFileToken:
|
|
const cls = tryCast(lastOrUndefined(cast(location.parent, isSourceFile).statements), isObjectTypeDeclaration);
|
|
if (cls && !findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile)) {
|
|
return cls;
|
|
}
|
|
}
|
|
|
|
if (!contextToken) return undefined;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.SemicolonToken: // class c {getValue(): number; | }
|
|
case SyntaxKind.CloseBraceToken: // class c { method() { } | }
|
|
// class c { method() { } b| }
|
|
return isFromObjectTypeDeclaration(location) && (location.parent as ClassElement | TypeElement).name === location
|
|
? location.parent.parent as ObjectTypeDeclaration
|
|
: tryCast(location, isObjectTypeDeclaration);
|
|
case SyntaxKind.OpenBraceToken: // class c { |
|
|
case SyntaxKind.CommaToken: // class c {getValue(): number, | }
|
|
return tryCast(contextToken.parent, isObjectTypeDeclaration);
|
|
default:
|
|
if (!isFromObjectTypeDeclaration(contextToken)) return undefined;
|
|
const isValidKeyword = isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword;
|
|
return (isValidKeyword(contextToken.kind) || contextToken.kind === SyntaxKind.AsteriskToken || isIdentifier(contextToken) && isValidKeyword(stringToToken(contextToken.text)!)) // TODO: GH#18217
|
|
? contextToken.parent.parent as ObjectTypeDeclaration : undefined;
|
|
}
|
|
}
|
|
|
|
// TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes
|
|
function isFromObjectTypeDeclaration(node: Node): boolean {
|
|
return node.parent && isClassOrTypeElement(node.parent) && isObjectTypeDeclaration(node.parent.parent);
|
|
}
|
|
|
|
function isValidTrigger(sourceFile: SourceFile, triggerCharacter: CompletionsTriggerCharacter, contextToken: Node | undefined, position: number): boolean {
|
|
switch (triggerCharacter) {
|
|
case ".":
|
|
case "@":
|
|
return true;
|
|
case '"':
|
|
case "'":
|
|
case "`":
|
|
// Only automatically bring up completions if this is an opening quote.
|
|
return !!contextToken && isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1;
|
|
case "<":
|
|
// Opening JSX tag
|
|
return !!contextToken && contextToken.kind === SyntaxKind.LessThanToken && (!isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent));
|
|
case "/":
|
|
return !!contextToken && (isStringLiteralLike(contextToken)
|
|
? !!tryGetImportFromModuleSpecifier(contextToken)
|
|
: contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent));
|
|
default:
|
|
return Debug.assertNever(triggerCharacter);
|
|
}
|
|
}
|
|
|
|
function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean {
|
|
return nodeIsMissing(left);
|
|
}
|
|
}
|