mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-25 12:40:05 -05:00
Support completions that require changing from dot to bracket access (#20547)
* Support completions that require changing from dot to bracket access * Use insertText and replacementSpan * Rename includeBracketCompletions to includeInsertTextCompletions * Don't add completions that start with space
This commit is contained in:
@@ -511,7 +511,7 @@ namespace FourSlash {
|
||||
}
|
||||
}
|
||||
|
||||
private raiseError(message: string) {
|
||||
private raiseError(message: string): never {
|
||||
throw new Error(this.messageAtLastKnownMarker(message));
|
||||
}
|
||||
|
||||
@@ -848,10 +848,10 @@ namespace FourSlash {
|
||||
}
|
||||
}
|
||||
|
||||
public verifyCompletionsAt(markerName: string, expected: string[], options?: FourSlashInterface.CompletionsAtOptions) {
|
||||
public verifyCompletionsAt(markerName: string, expected: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntry>, options?: FourSlashInterface.CompletionsAtOptions) {
|
||||
this.goToMarker(markerName);
|
||||
|
||||
const actualCompletions = this.getCompletionListAtCaret();
|
||||
const actualCompletions = this.getCompletionListAtCaret(options);
|
||||
if (!actualCompletions) {
|
||||
this.raiseError(`No completions at position '${this.currentCaretPosition}'.`);
|
||||
}
|
||||
@@ -867,9 +867,20 @@ namespace FourSlash {
|
||||
}
|
||||
|
||||
ts.zipWith(actual, expected, (completion, expectedCompletion, index) => {
|
||||
if (completion.name !== expectedCompletion) {
|
||||
const { name, insertText, replacementSpan } = typeof expectedCompletion === "string" ? { name: expectedCompletion, insertText: undefined, replacementSpan: undefined } : expectedCompletion;
|
||||
if (completion.name !== name) {
|
||||
this.raiseError(`Expected completion at index ${index} to be ${expectedCompletion}, got ${completion.name}`);
|
||||
}
|
||||
if (completion.insertText !== insertText) {
|
||||
this.raiseError(`Expected completion insert text at index ${index} to be ${insertText}, got ${completion.insertText}`);
|
||||
}
|
||||
const convertedReplacementSpan = replacementSpan && textSpanFromRange(replacementSpan);
|
||||
try {
|
||||
assert.deepEqual(completion.replacementSpan, convertedReplacementSpan);
|
||||
}
|
||||
catch {
|
||||
this.raiseError(`Expected completion replacementSpan at index ${index} to be ${stringify(convertedReplacementSpan)}, got ${stringify(completion.replacementSpan)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1808,7 +1819,7 @@ Actual: ${stringify(fullActual)}`);
|
||||
}
|
||||
else if (prevChar === " " && /A-Za-z_/.test(ch)) {
|
||||
/* Completions */
|
||||
this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, { includeExternalModuleExports: false });
|
||||
this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
}
|
||||
|
||||
if (i % checkCadence === 0) {
|
||||
@@ -2383,7 +2394,8 @@ Actual: ${stringify(fullActual)}`);
|
||||
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
|
||||
this.goToMarker(markerName);
|
||||
|
||||
const actualCompletion = this.getCompletionListAtCaret({ includeExternalModuleExports: true }).entries.find(e => e.name === options.name && e.source === options.source);
|
||||
const actualCompletion = this.getCompletionListAtCaret({ includeExternalModuleExports: true, includeInsertTextCompletions: false }).entries.find(e =>
|
||||
e.name === options.name && e.source === options.source);
|
||||
|
||||
if (!actualCompletion.hasAction) {
|
||||
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
|
||||
@@ -3195,8 +3207,7 @@ Actual: ${stringify(fullActual)}`);
|
||||
private getTextSpanForRangeAtIndex(index: number): ts.TextSpan {
|
||||
const ranges = this.getRanges();
|
||||
if (ranges && ranges.length > index) {
|
||||
const range = ranges[index];
|
||||
return { start: range.start, length: range.end - range.start };
|
||||
return textSpanFromRange(ranges[index]);
|
||||
}
|
||||
else {
|
||||
this.raiseError("Supplied span index: " + index + " does not exist in range list of size: " + (ranges ? 0 : ranges.length));
|
||||
@@ -3226,6 +3237,10 @@ Actual: ${stringify(fullActual)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function textSpanFromRange(range: FourSlash.Range): ts.TextSpan {
|
||||
return ts.createTextSpanFromBounds(range.start, range.end);
|
||||
}
|
||||
|
||||
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
|
||||
const content = Harness.IO.readFile(fileName);
|
||||
runFourSlashTestContent(basePath, testType, content, fileName);
|
||||
@@ -3967,7 +3982,7 @@ namespace FourSlashInterface {
|
||||
super(state);
|
||||
}
|
||||
|
||||
public completionsAt(markerName: string, completions: string[], options?: CompletionsAtOptions) {
|
||||
public completionsAt(markerName: string, completions: ReadonlyArray<ExpectedCompletionEntry>, options?: CompletionsAtOptions) {
|
||||
this.state.verifyCompletionsAt(markerName, completions, options);
|
||||
}
|
||||
|
||||
@@ -4591,6 +4606,7 @@ namespace FourSlashInterface {
|
||||
newContent: string;
|
||||
}
|
||||
|
||||
export type ExpectedCompletionEntry = string | { name: string, insertText?: string, replacementSpan?: FourSlash.Range };
|
||||
export interface CompletionsAtOptions extends ts.GetCompletionsAtPositionOptions {
|
||||
isNewIdentifierLocation?: boolean;
|
||||
}
|
||||
|
||||
@@ -1302,13 +1302,13 @@ namespace ts.projectSystem {
|
||||
service.checkNumberOfProjects({ externalProjects: 1 });
|
||||
checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
|
||||
|
||||
const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false });
|
||||
const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
// should contain completions for string
|
||||
assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'");
|
||||
assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'");
|
||||
|
||||
service.closeClientFile(f2.path);
|
||||
const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false });
|
||||
const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
// should contain completions for string
|
||||
assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'");
|
||||
assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'");
|
||||
@@ -1334,11 +1334,11 @@ namespace ts.projectSystem {
|
||||
service.checkNumberOfProjects({ externalProjects: 1 });
|
||||
checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
|
||||
|
||||
const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false });
|
||||
const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'");
|
||||
|
||||
service.closeClientFile(f2.path);
|
||||
const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false });
|
||||
const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'");
|
||||
const sf2 = service.externalProjects[0].getLanguageService().getProgram().getSourceFile(f2.path);
|
||||
assert.equal(sf2.text, "");
|
||||
@@ -1943,7 +1943,7 @@ namespace ts.projectSystem {
|
||||
|
||||
// Check identifiers defined in HTML content are available in .ts file
|
||||
const project = configuredProjectAt(projectService, 0);
|
||||
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, { includeExternalModuleExports: false });
|
||||
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`);
|
||||
|
||||
// Close HTML file
|
||||
@@ -1957,7 +1957,7 @@ namespace ts.projectSystem {
|
||||
checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
|
||||
|
||||
// Check identifiers defined in HTML content are not available in .ts file
|
||||
completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, { includeExternalModuleExports: false });
|
||||
completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
|
||||
});
|
||||
|
||||
|
||||
@@ -1693,6 +1693,11 @@ namespace ts.server.protocol {
|
||||
* This affects lone identifier completions but not completions on the right hand side of `obj.`.
|
||||
*/
|
||||
includeExternalModuleExports: boolean;
|
||||
/**
|
||||
* If enabled, the completion list will include completions with invalid identifier names.
|
||||
* For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`.
|
||||
*/
|
||||
includeInsertTextCompletions: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1768,6 +1773,12 @@ namespace ts.server.protocol {
|
||||
* is often the same as the name but may be different in certain circumstances.
|
||||
*/
|
||||
sortText: string;
|
||||
/**
|
||||
* Text to insert instead of `name`.
|
||||
* This is used to support bracketed completions; If `name` might be "a-b" but `insertText` would be `["a-b"]`,
|
||||
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
|
||||
*/
|
||||
insertText?: string;
|
||||
/**
|
||||
* An optional span that indicates the text to be replaced by this completion item.
|
||||
* If present, this span should be used instead of the default one.
|
||||
|
||||
@@ -1207,10 +1207,10 @@ namespace ts.server {
|
||||
if (simplifiedResult) {
|
||||
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
|
||||
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
|
||||
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry;
|
||||
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended } = entry;
|
||||
const convertedSpan = replacementSpan ? this.toLocationTextSpan(replacementSpan, scriptInfo) : undefined;
|
||||
// Use `hasAction || undefined` to avoid serializing `false`.
|
||||
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended };
|
||||
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended };
|
||||
}
|
||||
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
|
||||
}
|
||||
|
||||
@@ -50,40 +50,49 @@ namespace ts.Completions {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion } = completionData;
|
||||
switch (completionData.kind) {
|
||||
case CompletionDataKind.Data:
|
||||
return completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, options.includeInsertTextCompletions);
|
||||
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:
|
||||
throw Debug.assertNever(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceFile.languageVariant === LanguageVariant.JSX &&
|
||||
location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) {
|
||||
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, includeInsertTextCompletions: boolean): CompletionInfo {
|
||||
const { symbols, completionKind, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion } = completionData;
|
||||
|
||||
if (sourceFile.languageVariant === LanguageVariant.JSX && 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
|
||||
const tagName = (<JsxElement>location.parent.parent).openingElement.tagName;
|
||||
const tagName = location.parent.parent.openingElement.tagName;
|
||||
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false,
|
||||
entries: [{
|
||||
name: (<JsxTagNameExpression>tagName).getFullText(),
|
||||
name: tagName.getFullText(),
|
||||
kind: ScriptElementKind.classElement,
|
||||
kindModifiers: undefined,
|
||||
sortText: "0",
|
||||
}]};
|
||||
}
|
||||
|
||||
if (request) {
|
||||
const entries = request.kind === "JsDocTagName"
|
||||
// If the current position is a jsDoc tag name, only tag names should be provided for completion
|
||||
? JsDoc.getJSDocTagNameCompletions()
|
||||
: request.kind === "JsDocTag"
|
||||
// If the current position is a jsDoc tag, only tags should be provided for completion
|
||||
? JsDoc.getJSDocTagCompletions()
|
||||
: JsDoc.getJSDocParameterNameCompletions(request.tag);
|
||||
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
||||
}
|
||||
|
||||
const entries: CompletionEntry[] = [];
|
||||
|
||||
if (isSourceFileJavaScript(sourceFile)) {
|
||||
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, recommendedCompletion, symbolToOriginInfoMap);
|
||||
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, recommendedCompletion, symbolToOriginInfoMap);
|
||||
getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries);
|
||||
}
|
||||
else {
|
||||
@@ -91,7 +100,7 @@ namespace ts.Completions {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, recommendedCompletion, symbolToOriginInfoMap);
|
||||
getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, recommendedCompletion, symbolToOriginInfoMap);
|
||||
}
|
||||
|
||||
// TODO add filter for keyword based on type/value/namespace and also location
|
||||
@@ -99,17 +108,29 @@ namespace ts.Completions {
|
||||
// Add all keywords if
|
||||
// - this is not a member completion list (all the keywords)
|
||||
// - other filters are enabled in required scenario so add those keywords
|
||||
const isMemberCompletion = isMemberCompletionKind(completionKind);
|
||||
if (keywordFilters !== KeywordCompletionFilters.None || !isMemberCompletion) {
|
||||
addRange(entries, getKeywordCompletions(keywordFilters));
|
||||
}
|
||||
|
||||
return { isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, entries };
|
||||
return { isGlobalCompletion: completionKind === CompletionKind.Global, isMemberCompletion, isNewIdentifierLocation, entries };
|
||||
}
|
||||
|
||||
function isMemberCompletionKind(kind: CompletionKind): boolean {
|
||||
switch (kind) {
|
||||
case CompletionKind.ObjectPropertyDeclaration:
|
||||
case CompletionKind.MemberLike:
|
||||
case CompletionKind.PropertyAccess:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getJavaScriptCompletionEntries(
|
||||
sourceFile: SourceFile,
|
||||
position: number,
|
||||
uniqueNames: Map<{}>,
|
||||
uniqueNames: Map<true>,
|
||||
target: ScriptTarget,
|
||||
entries: Push<CompletionEntry>): void {
|
||||
getNameTable(sourceFile).forEach((pos, name) => {
|
||||
@@ -118,16 +139,9 @@ namespace ts.Completions {
|
||||
return;
|
||||
}
|
||||
const realName = unescapeLeadingUnderscores(name);
|
||||
|
||||
if (uniqueNames.has(realName) || isStringANonContextualKeyword(realName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueNames.set(realName, true);
|
||||
const displayName = getCompletionEntryDisplayName(realName, target, /*performCharacterChecks*/ true, /*allowStringLiteral*/ false);
|
||||
if (displayName) {
|
||||
if (addToSeen(uniqueNames, realName) && isIdentifierText(realName, target) && !isStringANonContextualKeyword(realName)) {
|
||||
entries.push({
|
||||
name: displayName,
|
||||
name: realName,
|
||||
kind: ScriptElementKind.warning,
|
||||
kindModifiers: "",
|
||||
sortText: "1"
|
||||
@@ -139,18 +153,22 @@ namespace ts.Completions {
|
||||
function createCompletionEntry(
|
||||
symbol: Symbol,
|
||||
location: Node,
|
||||
performCharacterChecks: boolean,
|
||||
sourceFile: SourceFile,
|
||||
typeChecker: TypeChecker,
|
||||
target: ScriptTarget,
|
||||
allowStringLiteral: boolean,
|
||||
kind: CompletionKind,
|
||||
origin: SymbolOriginInfo | undefined,
|
||||
recommendedCompletion: Symbol | undefined,
|
||||
propertyAccessToConvert: PropertyAccessExpression | undefined,
|
||||
includeInsertTextCompletions: boolean,
|
||||
): CompletionEntry | undefined {
|
||||
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
|
||||
// We would like to only show things that can be added after a dot, so for instance numeric properties can
|
||||
// not be accessed with a dot (a.1 <- invalid)
|
||||
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks, allowStringLiteral, origin);
|
||||
if (!displayName) {
|
||||
const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind);
|
||||
if (!info) {
|
||||
return undefined;
|
||||
}
|
||||
const { name, needsConvertPropertyAccess } = info;
|
||||
Debug.assert(!(needsConvertPropertyAccess && !propertyAccessToConvert));
|
||||
if (needsConvertPropertyAccess && !includeInsertTextCompletions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -163,16 +181,22 @@ namespace ts.Completions {
|
||||
// Use a 'sortText' of 0' so that all symbol completion entries come before any other
|
||||
// entries (like JavaScript identifier entries).
|
||||
return {
|
||||
name: displayName,
|
||||
name,
|
||||
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location),
|
||||
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
|
||||
sortText: "0",
|
||||
source: getSourceFromOrigin(origin),
|
||||
hasAction: trueOrUndefined(origin !== undefined),
|
||||
// TODO: GH#20619 Use configured quote style
|
||||
insertText: needsConvertPropertyAccess ? `["${name}"]` : undefined,
|
||||
replacementSpan: needsConvertPropertyAccess
|
||||
? createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert.name.end)
|
||||
: undefined,
|
||||
hasAction: trueOrUndefined(needsConvertPropertyAccess || origin !== undefined),
|
||||
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol, checker: TypeChecker): boolean {
|
||||
return localSymbol === recommendedCompletion ||
|
||||
!!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion;
|
||||
@@ -190,11 +214,13 @@ namespace ts.Completions {
|
||||
symbols: ReadonlyArray<Symbol>,
|
||||
entries: Push<CompletionEntry>,
|
||||
location: Node,
|
||||
performCharacterChecks: boolean,
|
||||
sourceFile: SourceFile,
|
||||
typeChecker: TypeChecker,
|
||||
target: ScriptTarget,
|
||||
log: Log,
|
||||
allowStringLiteral: boolean,
|
||||
kind: CompletionKind,
|
||||
includeInsertTextCompletions?: boolean,
|
||||
propertyAccessToConvert?: PropertyAccessExpression | undefined,
|
||||
recommendedCompletion?: Symbol,
|
||||
symbolToOriginInfoMap?: SymbolOriginInfoMap,
|
||||
): Map<true> {
|
||||
@@ -206,7 +232,7 @@ namespace ts.Completions {
|
||||
const uniques = createMap<true>();
|
||||
for (const symbol of symbols) {
|
||||
const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined;
|
||||
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral, origin, recommendedCompletion);
|
||||
const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, includeInsertTextCompletions);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
@@ -256,7 +282,7 @@ namespace ts.Completions {
|
||||
// foo({
|
||||
// '/*completion position*/'
|
||||
// });
|
||||
return getStringLiteralCompletionEntriesFromPropertyAssignment(<ObjectLiteralElement>node.parent, typeChecker, compilerOptions.target, log);
|
||||
return getStringLiteralCompletionEntriesFromPropertyAssignment(<ObjectLiteralElement>node.parent, sourceFile, typeChecker, compilerOptions.target, log);
|
||||
}
|
||||
else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) {
|
||||
// Get all names of properties on the expression
|
||||
@@ -266,7 +292,7 @@ namespace ts.Completions {
|
||||
// let a: A;
|
||||
// a['/*completion position*/']
|
||||
const type = typeChecker.getTypeAtLocation(node.parent.expression);
|
||||
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, type, typeChecker, compilerOptions.target, log);
|
||||
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
|
||||
}
|
||||
else if (node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration
|
||||
|| isRequireCall(node.parent, /*checkArgumentIsStringLiteral*/ false) || isImportCall(node.parent)
|
||||
@@ -288,7 +314,7 @@ namespace ts.Completions {
|
||||
// }
|
||||
// let x: Foo["/*completion position*/"]
|
||||
const type = typeChecker.getTypeFromTypeNode(node.parent.parent.objectType);
|
||||
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, type, typeChecker, compilerOptions.target, log);
|
||||
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
|
||||
}
|
||||
else {
|
||||
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
|
||||
@@ -317,11 +343,11 @@ namespace ts.Completions {
|
||||
};
|
||||
}
|
||||
|
||||
function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
||||
function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
||||
const type = typeChecker.getContextualType((<ObjectLiteralExpression>element.parent));
|
||||
const entries: CompletionEntry[] = [];
|
||||
if (type) {
|
||||
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/ false, typeChecker, target, log, /*allowStringLiteral*/ true);
|
||||
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, sourceFile, typeChecker, target, log, CompletionKind.String);
|
||||
if (entries.length) {
|
||||
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
|
||||
}
|
||||
@@ -346,10 +372,10 @@ namespace ts.Completions {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(stringLiteralNode: StringLiteral | NoSubstitutionTemplateLiteral, type: Type, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
||||
function getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(stringLiteralNode: StringLiteral | NoSubstitutionTemplateLiteral, sourceFile: SourceFile, type: Type, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
||||
const entries: CompletionEntry[] = [];
|
||||
if (type) {
|
||||
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, stringLiteralNode, /*performCharacterChecks*/ false, typeChecker, target, log, /*allowStringLiteral*/ true);
|
||||
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, stringLiteralNode, sourceFile, typeChecker, target, log, CompletionKind.String);
|
||||
if (entries.length) {
|
||||
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
|
||||
}
|
||||
@@ -420,6 +446,13 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
|
||||
interface SymbolCompletion {
|
||||
type: "symbol";
|
||||
symbol: Symbol;
|
||||
location: Node;
|
||||
symbolToOriginInfoMap: SymbolOriginInfoMap;
|
||||
previousToken: Node;
|
||||
}
|
||||
function getSymbolCompletionFromEntryId(
|
||||
typeChecker: TypeChecker,
|
||||
log: (message: string) => void,
|
||||
@@ -428,27 +461,26 @@ namespace ts.Completions {
|
||||
position: number,
|
||||
{ name, source }: CompletionEntryIdentifier,
|
||||
allSourceFiles: ReadonlyArray<SourceFile>,
|
||||
): { type: "symbol", symbol: Symbol, location: Node, symbolToOriginInfoMap: SymbolOriginInfoMap, previousToken: Node } | { type: "request", request: Request } | { type: "none" } {
|
||||
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeExternalModuleExports: true }, compilerOptions.target);
|
||||
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
|
||||
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeExternalModuleExports: true, includeInsertTextCompletions: true }, compilerOptions.target);
|
||||
if (!completionData) {
|
||||
return { type: "none" };
|
||||
}
|
||||
|
||||
const { symbols, location, allowStringLiteral, symbolToOriginInfoMap, request, previousToken } = completionData;
|
||||
if (request) {
|
||||
return { type: "request", request };
|
||||
if (completionData.kind !== CompletionDataKind.Data) {
|
||||
return { type: "request", request: completionData };
|
||||
}
|
||||
|
||||
const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken } = completionData;
|
||||
|
||||
// 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.
|
||||
const symbol = find(symbols, s => {
|
||||
const origin = symbolToOriginInfoMap[getSymbolId(s)];
|
||||
return getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral, origin) === name
|
||||
&& getSourceFromOrigin(origin) === source;
|
||||
});
|
||||
return symbol ? { type: "symbol", symbol, location, symbolToOriginInfoMap, previousToken } : { type: "none" };
|
||||
return firstDefined<Symbol, SymbolCompletion>(symbols, (symbol): SymbolCompletion => { // 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 === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken } : undefined;
|
||||
}) || { type: "none" };
|
||||
}
|
||||
|
||||
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
|
||||
@@ -484,11 +516,11 @@ namespace ts.Completions {
|
||||
case "request": {
|
||||
const { request } = symbolCompletion;
|
||||
switch (request.kind) {
|
||||
case "JsDocTagName":
|
||||
case CompletionDataKind.JsDocTagName:
|
||||
return JsDoc.getJSDocTagNameCompletionDetails(name);
|
||||
case "JsDocTag":
|
||||
case CompletionDataKind.JsDocTag:
|
||||
return JsDoc.getJSDocTagCompletionDetails(name);
|
||||
case "JsDocParameterName":
|
||||
case CompletionDataKind.JsDocParameterName:
|
||||
return JsDoc.getJSDocParameterNameCompletionDetails(name);
|
||||
default:
|
||||
return Debug.assertNever(request);
|
||||
@@ -520,6 +552,10 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
|
||||
interface CodeActionsAndSourceDisplay {
|
||||
readonly codeActions: CodeAction[] | undefined;
|
||||
readonly sourceDisplay: SymbolDisplayPart[] | undefined;
|
||||
}
|
||||
function getCompletionEntryCodeActionsAndSourceDisplay(
|
||||
symbolToOriginInfoMap: SymbolOriginInfoMap,
|
||||
symbol: Symbol,
|
||||
@@ -532,12 +568,26 @@ namespace ts.Completions {
|
||||
formatContext: formatting.FormatContext,
|
||||
getCanonicalFileName: GetCanonicalFileName,
|
||||
allSourceFiles: ReadonlyArray<SourceFile>,
|
||||
): { codeActions: CodeAction[] | undefined, sourceDisplay: SymbolDisplayPart[] | undefined } {
|
||||
): CodeActionsAndSourceDisplay {
|
||||
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
|
||||
if (!symbolOriginInfo) {
|
||||
return { codeActions: undefined, sourceDisplay: undefined };
|
||||
}
|
||||
return symbolOriginInfo
|
||||
? getCodeActionsAndSourceDisplayForImport(symbolOriginInfo, symbol, program, checker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles)
|
||||
: { codeActions: undefined, sourceDisplay: undefined };
|
||||
}
|
||||
|
||||
function getCodeActionsAndSourceDisplayForImport(
|
||||
symbolOriginInfo: SymbolOriginInfo,
|
||||
symbol: Symbol,
|
||||
program: Program,
|
||||
checker: TypeChecker,
|
||||
host: LanguageServiceHost,
|
||||
compilerOptions: CompilerOptions,
|
||||
sourceFile: SourceFile,
|
||||
previousToken: Node,
|
||||
formatContext: formatting.FormatContext,
|
||||
getCanonicalFileName: GetCanonicalFileName,
|
||||
allSourceFiles: ReadonlyArray<SourceFile>
|
||||
): CodeActionsAndSourceDisplay {
|
||||
const { moduleSymbol, isDefaultExport } = symbolOriginInfo;
|
||||
const exportedSymbol = skipAlias(symbol.exportSymbol || symbol, checker);
|
||||
const moduleSymbols = getAllReExportingModules(exportedSymbol, checker, allSourceFiles);
|
||||
@@ -585,21 +635,30 @@ namespace ts.Completions {
|
||||
return completion.type === "symbol" ? completion.symbol : undefined;
|
||||
}
|
||||
|
||||
const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName }
|
||||
interface CompletionData {
|
||||
symbols: ReadonlyArray<Symbol>;
|
||||
isGlobalCompletion: boolean;
|
||||
isMemberCompletion: boolean;
|
||||
allowStringLiteral: boolean;
|
||||
isNewIdentifierLocation: boolean;
|
||||
location: Node | undefined;
|
||||
isRightOfDot: boolean;
|
||||
request?: Request;
|
||||
keywordFilters: KeywordCompletionFilters;
|
||||
symbolToOriginInfoMap: SymbolOriginInfoMap;
|
||||
recommendedCompletion: Symbol | undefined;
|
||||
previousToken: Node;
|
||||
readonly kind: CompletionDataKind.Data;
|
||||
readonly symbols: ReadonlyArray<Symbol>;
|
||||
readonly completionKind: CompletionKind;
|
||||
/** 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 symbolToOriginInfoMap: SymbolOriginInfoMap;
|
||||
readonly recommendedCompletion: Symbol | undefined;
|
||||
readonly previousToken: Node | undefined;
|
||||
}
|
||||
type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag };
|
||||
|
||||
const enum CompletionKind {
|
||||
ObjectPropertyDeclaration,
|
||||
Global,
|
||||
PropertyAccess,
|
||||
MemberLike,
|
||||
String,
|
||||
None,
|
||||
}
|
||||
type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag };
|
||||
|
||||
function getRecommendedCompletion(currentToken: Node, checker: TypeChecker): Symbol | undefined {
|
||||
const ty = getContextualType(currentToken, checker);
|
||||
@@ -670,9 +729,7 @@ namespace ts.Completions {
|
||||
allSourceFiles: ReadonlyArray<SourceFile>,
|
||||
options: GetCompletionsAtPositionOptions,
|
||||
target: ScriptTarget,
|
||||
): CompletionData | undefined {
|
||||
let request: Request | undefined;
|
||||
|
||||
): CompletionData | Request | undefined {
|
||||
let start = timestamp();
|
||||
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // 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.)
|
||||
@@ -690,7 +747,7 @@ namespace ts.Completions {
|
||||
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
|
||||
request = { kind: "JsDocTagName" };
|
||||
return { kind: CompletionDataKind.JsDocTagName };
|
||||
}
|
||||
else {
|
||||
// When completion is requested without "@", we will have check to make sure that
|
||||
@@ -711,7 +768,7 @@ namespace ts.Completions {
|
||||
// */
|
||||
const lineStart = getLineStartPositionForPosition(position, sourceFile);
|
||||
if (!(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/))) {
|
||||
request = { kind: "JsDocTag" };
|
||||
return { kind: CompletionDataKind.JsDocTag };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,7 +779,7 @@ namespace ts.Completions {
|
||||
const tag = getJsDocTagAtPosition(currentToken, position);
|
||||
if (tag) {
|
||||
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
|
||||
request = { kind: "JsDocTagName" };
|
||||
return { kind: CompletionDataKind.JsDocTagName };
|
||||
}
|
||||
if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
|
||||
currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true);
|
||||
@@ -735,27 +792,10 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
|
||||
request = { kind: "JsDocParameterName", tag };
|
||||
return { kind: CompletionDataKind.JsDocParameterName, tag };
|
||||
}
|
||||
}
|
||||
|
||||
if (request) {
|
||||
return {
|
||||
symbols: emptyArray,
|
||||
isGlobalCompletion: false,
|
||||
isMemberCompletion: false,
|
||||
allowStringLiteral: false,
|
||||
isNewIdentifierLocation: false,
|
||||
location: undefined,
|
||||
isRightOfDot: false,
|
||||
request,
|
||||
keywordFilters: KeywordCompletionFilters.None,
|
||||
symbolToOriginInfoMap: undefined,
|
||||
recommendedCompletion: undefined,
|
||||
previousToken: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
@@ -784,6 +824,7 @@ namespace ts.Completions {
|
||||
// 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;
|
||||
@@ -798,18 +839,19 @@ namespace ts.Completions {
|
||||
|
||||
let parent = contextToken.parent;
|
||||
if (contextToken.kind === SyntaxKind.DotToken) {
|
||||
if (parent.kind === SyntaxKind.PropertyAccessExpression) {
|
||||
node = (<PropertyAccessExpression>contextToken.parent).expression;
|
||||
isRightOfDot = true;
|
||||
}
|
||||
else if (parent.kind === SyntaxKind.QualifiedName) {
|
||||
node = (<QualifiedName>contextToken.parent).left;
|
||||
isRightOfDot = true;
|
||||
}
|
||||
else {
|
||||
// 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;
|
||||
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;
|
||||
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) {
|
||||
@@ -849,10 +891,8 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
const semanticStart = timestamp();
|
||||
let isGlobalCompletion = false;
|
||||
let isMemberCompletion: boolean;
|
||||
let allowStringLiteral = false;
|
||||
let isNewIdentifierLocation: boolean;
|
||||
let completionKind = CompletionKind.None;
|
||||
let isNewIdentifierLocation = false;
|
||||
let keywordFilters = KeywordCompletionFilters.None;
|
||||
let symbols: Symbol[] = [];
|
||||
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
|
||||
@@ -868,8 +908,7 @@ namespace ts.Completions {
|
||||
else {
|
||||
symbols = tagSymbols;
|
||||
}
|
||||
isMemberCompletion = true;
|
||||
isNewIdentifierLocation = false;
|
||||
completionKind = CompletionKind.MemberLike;
|
||||
}
|
||||
else if (isStartingCloseTag) {
|
||||
const tagName = (<JsxElement>contextToken.parent.parent).openingElement.tagName;
|
||||
@@ -878,8 +917,7 @@ namespace ts.Completions {
|
||||
if (!typeChecker.isUnknownSymbol(tagSymbol)) {
|
||||
symbols = [tagSymbol];
|
||||
}
|
||||
isMemberCompletion = true;
|
||||
isNewIdentifierLocation = false;
|
||||
completionKind = CompletionKind.MemberLike;
|
||||
}
|
||||
else {
|
||||
// For JavaScript or TypeScript, if we're not after a dot, then just try to get the
|
||||
@@ -893,7 +931,7 @@ namespace ts.Completions {
|
||||
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
|
||||
|
||||
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker);
|
||||
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken };
|
||||
return { kind: CompletionDataKind.Data, symbols, completionKind, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken };
|
||||
|
||||
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
|
||||
|
||||
@@ -910,9 +948,7 @@ namespace ts.Completions {
|
||||
|
||||
function getTypeScriptMemberSymbols(): void {
|
||||
// Right of dot member completion list
|
||||
isGlobalCompletion = false;
|
||||
isMemberCompletion = true;
|
||||
isNewIdentifierLocation = false;
|
||||
completionKind = CompletionKind.PropertyAccess;
|
||||
|
||||
// Since this is qualified name check its a type node location
|
||||
const isTypeLocation = insideJsDocTagTypeExpression || isPartOfTypeNode(node.parent);
|
||||
@@ -988,7 +1024,7 @@ namespace ts.Completions {
|
||||
|
||||
if (tryGetConstructorLikeCompletionContainer(contextToken)) {
|
||||
// no members, only keywords
|
||||
isMemberCompletion = false;
|
||||
completionKind = CompletionKind.None;
|
||||
// Declaring new property/method/accessor
|
||||
isNewIdentifierLocation = true;
|
||||
// Has keywords for constructor parameter
|
||||
@@ -1010,7 +1046,7 @@ namespace ts.Completions {
|
||||
|
||||
if (attrsType) {
|
||||
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>jsxContainer).attributes.properties);
|
||||
isMemberCompletion = true;
|
||||
completionKind = CompletionKind.MemberLike;
|
||||
isNewIdentifierLocation = false;
|
||||
return true;
|
||||
}
|
||||
@@ -1018,7 +1054,7 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
// Get all entities in the current scope.
|
||||
isMemberCompletion = false;
|
||||
completionKind = CompletionKind.None;
|
||||
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken);
|
||||
|
||||
if (previousToken !== contextToken) {
|
||||
@@ -1054,13 +1090,8 @@ namespace ts.Completions {
|
||||
position;
|
||||
|
||||
const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile;
|
||||
if (scopeNode) {
|
||||
isGlobalCompletion =
|
||||
scopeNode.kind === SyntaxKind.SourceFile ||
|
||||
scopeNode.kind === SyntaxKind.TemplateExpression ||
|
||||
scopeNode.kind === SyntaxKind.JsxExpression ||
|
||||
scopeNode.kind === SyntaxKind.Block || // Some blocks aren't statements, but all get global completions
|
||||
isStatement(scopeNode);
|
||||
if (isGlobalCompletionScope(scopeNode)) {
|
||||
completionKind = CompletionKind.Global;
|
||||
}
|
||||
|
||||
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
|
||||
@@ -1074,6 +1105,18 @@ namespace ts.Completions {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isGlobalCompletionScope(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 {
|
||||
filterMutate(symbols, symbol => {
|
||||
if (!isSourceFile(location)) {
|
||||
@@ -1333,8 +1376,7 @@ namespace ts.Completions {
|
||||
*/
|
||||
function tryGetObjectLikeCompletionSymbols(objectLikeContainer: ObjectLiteralExpression | ObjectBindingPattern): boolean {
|
||||
// We're looking up possible property names from contextual/inferred/declared type.
|
||||
isMemberCompletion = true;
|
||||
allowStringLiteral = true;
|
||||
completionKind = CompletionKind.ObjectPropertyDeclaration;
|
||||
|
||||
let typeMembers: Symbol[];
|
||||
let existingMembers: ReadonlyArray<Declaration>;
|
||||
@@ -1412,7 +1454,7 @@ namespace ts.Completions {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMemberCompletion = true;
|
||||
completionKind = CompletionKind.MemberLike;
|
||||
isNewIdentifierLocation = false;
|
||||
|
||||
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier);
|
||||
@@ -1432,7 +1474,7 @@ namespace ts.Completions {
|
||||
*/
|
||||
function getGetClassLikeCompletionSymbols(classLikeDeclaration: ClassLikeDeclaration) {
|
||||
// We're looking up possible property names from parent type.
|
||||
isMemberCompletion = true;
|
||||
completionKind = CompletionKind.MemberLike;
|
||||
// Declaring new property/method/accessor
|
||||
isNewIdentifierLocation = true;
|
||||
// Has keywords for class elements
|
||||
@@ -1986,36 +2028,44 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name to be display in completion from a given symbol.
|
||||
*/
|
||||
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean, origin: SymbolOriginInfo | undefined): string | undefined {
|
||||
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);
|
||||
return name === undefined
|
||||
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)
|
||||
? undefined
|
||||
: getCompletionEntryDisplayName(name, target, performCharacterChecks, allowStringLiteral);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a displayName from a given for completion list, performing any necessary quotes stripping
|
||||
* and checking whether the name is valid identifier name.
|
||||
*/
|
||||
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean): string {
|
||||
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
|
||||
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
|
||||
// e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid.
|
||||
// We, thus, need to check if whatever was inside the quotes is actually a valid identifier name.
|
||||
if (performCharacterChecks && !isIdentifierText(name, target)) {
|
||||
// TODO: GH#18169
|
||||
return allowStringLiteral ? JSON.stringify(name) : undefined;
|
||||
|| isKnownSymbol(symbol)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return name;
|
||||
const validIdentiferResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false };
|
||||
if (isIdentifierText(name, target)) return validIdentiferResult;
|
||||
switch (kind) {
|
||||
case CompletionKind.None:
|
||||
case CompletionKind.Global:
|
||||
case CompletionKind.MemberLike:
|
||||
return undefined;
|
||||
case CompletionKind.ObjectPropertyDeclaration:
|
||||
// TODO: GH#18169
|
||||
return { name: JSON.stringify(name), needsConvertPropertyAccess: false };
|
||||
case CompletionKind.PropertyAccess:
|
||||
// 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.String:
|
||||
return validIdentiferResult;
|
||||
default:
|
||||
Debug.assertNever(kind);
|
||||
}
|
||||
}
|
||||
|
||||
// A cache of completion entries for keywords, these do not change between sessions
|
||||
@@ -2028,7 +2078,7 @@ namespace ts.Completions {
|
||||
return _keywordCompletions[keywordFilter] = generateKeywordCompletions(keywordFilter);
|
||||
|
||||
type FilterKeywordCompletions = (entryName: string) => boolean;
|
||||
function generateKeywordCompletions(keywordFilter: KeywordCompletionFilters) {
|
||||
function generateKeywordCompletions(keywordFilter: KeywordCompletionFilters): CompletionEntry[] {
|
||||
switch (keywordFilter) {
|
||||
case KeywordCompletionFilters.None:
|
||||
return getAllKeywordCompletions();
|
||||
@@ -2036,6 +2086,8 @@ namespace ts.Completions {
|
||||
return getFilteredKeywordCompletions(isClassMemberCompletionKeywordText);
|
||||
case KeywordCompletionFilters.ConstructorParameterKeywords:
|
||||
return getFilteredKeywordCompletions(isConstructorParameterCompletionKeywordText);
|
||||
default:
|
||||
Debug.assertNever(keywordFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1432,7 +1432,7 @@ namespace ts {
|
||||
return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)];
|
||||
}
|
||||
|
||||
function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = { includeExternalModuleExports: false }): CompletionInfo {
|
||||
function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = { includeExternalModuleExports: false, includeInsertTextCompletions: false }): CompletionInfo {
|
||||
synchronizeHostData();
|
||||
return Completions.getCompletionsAtPosition(
|
||||
host,
|
||||
|
||||
@@ -333,6 +333,7 @@ namespace ts {
|
||||
|
||||
export interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
}
|
||||
|
||||
export interface ApplyCodeActionCommandResult {
|
||||
@@ -748,6 +749,7 @@ namespace ts {
|
||||
kind: ScriptElementKind;
|
||||
kindModifiers: string; // see ScriptElementKindModifier, comma separated
|
||||
sortText: string;
|
||||
insertText?: string;
|
||||
/**
|
||||
* An optional span that indicates the text to be replaced by this completion item.
|
||||
* If present, this span should be used instead of the default one.
|
||||
|
||||
@@ -4001,6 +4001,7 @@ declare namespace ts {
|
||||
}
|
||||
interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
}
|
||||
interface ApplyCodeActionCommandResult {
|
||||
successMessage: string;
|
||||
@@ -4345,6 +4346,7 @@ declare namespace ts {
|
||||
kind: ScriptElementKind;
|
||||
kindModifiers: string;
|
||||
sortText: string;
|
||||
insertText?: string;
|
||||
/**
|
||||
* An optional span that indicates the text to be replaced by this completion item.
|
||||
* If present, this span should be used instead of the default one.
|
||||
@@ -6108,6 +6110,11 @@ declare namespace ts.server.protocol {
|
||||
* This affects lone identifier completions but not completions on the right hand side of `obj.`.
|
||||
*/
|
||||
includeExternalModuleExports: boolean;
|
||||
/**
|
||||
* If enabled, the completion list will include completions with invalid identifier names.
|
||||
* For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`.
|
||||
*/
|
||||
includeInsertTextCompletions: boolean;
|
||||
}
|
||||
/**
|
||||
* Completions request; value of command field is "completions".
|
||||
@@ -6176,6 +6183,12 @@ declare namespace ts.server.protocol {
|
||||
* is often the same as the name but may be different in certain circumstances.
|
||||
*/
|
||||
sortText: string;
|
||||
/**
|
||||
* Text to insert instead of `name`.
|
||||
* This is used to support bracketed completions; If `name` might be "a-b" but `insertText` would be `["a-b"]`,
|
||||
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
|
||||
*/
|
||||
insertText?: string;
|
||||
/**
|
||||
* An optional span that indicates the text to be replaced by this completion item.
|
||||
* If present, this span should be used instead of the default one.
|
||||
|
||||
@@ -4001,6 +4001,7 @@ declare namespace ts {
|
||||
}
|
||||
interface GetCompletionsAtPositionOptions {
|
||||
includeExternalModuleExports: boolean;
|
||||
includeInsertTextCompletions: boolean;
|
||||
}
|
||||
interface ApplyCodeActionCommandResult {
|
||||
successMessage: string;
|
||||
@@ -4345,6 +4346,7 @@ declare namespace ts {
|
||||
kind: ScriptElementKind;
|
||||
kindModifiers: string;
|
||||
sortText: string;
|
||||
insertText?: string;
|
||||
/**
|
||||
* An optional span that indicates the text to be replaced by this completion item.
|
||||
* If present, this span should be used instead of the default one.
|
||||
|
||||
@@ -11,8 +11,21 @@
|
||||
//// "\u0031\u0062": "invalid unicode identifer name (1b)"
|
||||
////};
|
||||
////
|
||||
////x./*a*/;
|
||||
////x[|./*a*/|];
|
||||
////x["/*b*/"];
|
||||
|
||||
verify.completionsAt("a", ["bar", "break", "any", "$", "b"]);
|
||||
verify.completionsAt("b", ["foo ", "bar", "break", "any", "#", "$", "b", "\u0031\u0062"]);
|
||||
verify.completionsAt("b", ["foo ", "bar", "break", "any", "#", "$", "b", "1b"]);
|
||||
|
||||
const replacementSpan = test.ranges()[0];
|
||||
verify.completionsAt("a", [
|
||||
{ name: "foo ", insertText: '["foo "]', replacementSpan },
|
||||
"bar",
|
||||
"break",
|
||||
"any",
|
||||
{ name: "#", insertText: '["#"]', replacementSpan },
|
||||
"$",
|
||||
"b",
|
||||
{ name: "1b", insertText: '["1b"]', replacementSpan },
|
||||
], {
|
||||
includeInsertTextCompletions: true,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////enum Foo {
|
||||
//// X, Y, '☆'
|
||||
////}
|
||||
////Foo./*a*/;
|
||||
////Foo["/*b*/"];
|
||||
// TODO: we should probably support this like we do in completionListInvalidMemberNames.ts
|
||||
|
||||
verify.completionsAt("a", ["X", "Y"]);
|
||||
verify.completionsAt("b", ["X", "Y", "☆"]);
|
||||
////declare var Symbol: SymbolConstructor;
|
||||
////interface SymbolConstructor {
|
||||
//// readonly hasInstance: symbol;
|
||||
////}
|
||||
////interface Function {
|
||||
//// [Symbol.hasInstance](value: any): boolean;
|
||||
////}
|
||||
////interface SomeInterface {
|
||||
//// (value: number): any;
|
||||
////}
|
||||
////var _ : SomeInterface;
|
||||
////_./**/
|
||||
|
||||
goTo.marker();
|
||||
verify.not.completionListContains("[Symbol.hasInstance]");
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @allowjs: true
|
||||
|
||||
// @Filename: test.js
|
||||
////interface Symbol {
|
||||
//// /** Returns a string representation of an object. */
|
||||
//// toString(): string;
|
||||
|
||||
//// /** Returns the primitive value of the specified object. */
|
||||
//// valueOf(): Object;
|
||||
////}
|
||||
|
||||
////interface SymbolConstructor {
|
||||
//// /**
|
||||
//// * A reference to the prototype.
|
||||
//// */
|
||||
//// readonly prototype: Symbol;
|
||||
|
||||
//// /**
|
||||
//// * Returns a new unique Symbol value.
|
||||
//// * @param description Description of the new Symbol object.
|
||||
//// */
|
||||
//// (description?: string | number): symbol;
|
||||
|
||||
//// /**
|
||||
//// * Returns a Symbol object from the global symbol registry matching the given key if found.
|
||||
//// * Otherwise, returns a new symbol with this key.
|
||||
//// * @param key key to search for.
|
||||
//// */
|
||||
//// for(key: string): symbol;
|
||||
|
||||
//// /**
|
||||
//// * Returns a key from the global symbol registry matching the given Symbol if found.
|
||||
//// * Otherwise, returns a undefined.
|
||||
//// * @param sym Symbol to find the key for.
|
||||
//// */
|
||||
//// keyFor(sym: symbol): string | undefined;
|
||||
////}
|
||||
|
||||
////declare var Symbol: SymbolConstructor;/// <reference path="lib.es2015.symbol.d.ts" />
|
||||
|
||||
////interface SymbolConstructor {
|
||||
//// /**
|
||||
//// * A method that determines if a constructor object recognizes an object as one of the
|
||||
//// * constructor’s instances. Called by the semantics of the instanceof operator.
|
||||
//// */
|
||||
//// readonly hasInstance: symbol;
|
||||
////}
|
||||
|
||||
////interface Function {
|
||||
//// /**
|
||||
//// * Determines whether the given value inherits from this function if this function was used
|
||||
//// * as a constructor function.
|
||||
//// *
|
||||
//// * A constructor function can control which objects are recognized as its instances by
|
||||
//// * 'instanceof' by overriding this method.
|
||||
//// */
|
||||
//// [Symbol.hasInstance](value: any): boolean;
|
||||
////}
|
||||
|
||||
////interface SomeInterface {
|
||||
//// (value: number): any;
|
||||
////}
|
||||
|
||||
////var _ : SomeInterface;
|
||||
////_./**/
|
||||
|
||||
goTo.marker();
|
||||
|
||||
verify.not.completionListContains("[Symbol.hasInstance]");
|
||||
@@ -0,0 +1,8 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////declare const x: { " foo": 0, "foo ": 1 };
|
||||
////x[|./**/|];
|
||||
|
||||
const replacementSpan = test.ranges()[0];
|
||||
// No completion for " foo" because it starts with a space. See https://github.com/Microsoft/TypeScript/pull/20547
|
||||
verify.completionsAt("", [{ name: "foo ", insertText: '["foo "]', replacementSpan }], { includeInsertTextCompletions: true });
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////declare const x: { "foo ": "space in the name", };
|
||||
////x[|.fo/**/|];
|
||||
|
||||
const replacementSpan = test.ranges()[0];
|
||||
verify.completionsAt("", [{ name: "foo ", insertText: '["foo "]', replacementSpan }], { includeInsertTextCompletions: true });
|
||||
@@ -1,13 +0,0 @@
|
||||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
//// enum e {
|
||||
//// "1",
|
||||
//// 2 = 3,
|
||||
//// 3,
|
||||
//// a,
|
||||
//// b
|
||||
//// }
|
||||
////
|
||||
//// e./**/
|
||||
|
||||
verify.completionsAt("", ["a", "b"]);
|
||||
@@ -12,19 +12,19 @@
|
||||
////not read
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
////import {} from "/*1*/";
|
||||
////import {} from "[|/*1*/|]";
|
||||
|
||||
// @Filename: /src/folder/b.ts
|
||||
////import {} from "x//*2*/";
|
||||
////import {} from "x/[|/*2*/|]";
|
||||
|
||||
// @Filename: /src/folder/c.ts
|
||||
////const foo = require("x//*3*/");
|
||||
////const foo = require("x/[|/*3*/|]");
|
||||
|
||||
// @Filename: /src/folder/4.ts
|
||||
////const foo = require(`x//*4*/`);
|
||||
|
||||
verify.completionsAt("1", ["y", "x"]);
|
||||
verify.completionsAt("2", ["bar", "foo"]);
|
||||
verify.completionsAt("3", ["bar", "foo"]);
|
||||
verify.completionsAt("4", ["bar", "foo"]);
|
||||
////const foo = require(`x/[|/*4*/|]`);
|
||||
|
||||
const [r0, r1, r2, r3] = test.ranges();
|
||||
verify.completionsAt("1", [{ name: "y", replacementSpan: r0 }, { name: "x", replacementSpan: r0 }]);
|
||||
verify.completionsAt("2", [{ name: "bar", replacementSpan: r1 }, { name: "foo", replacementSpan: r1 }]);
|
||||
verify.completionsAt("3", [{ name: "bar", replacementSpan: r2 }, { name: "foo", replacementSpan: r2 }]);
|
||||
verify.completionsAt("4", [{ name: "bar", replacementSpan: r3 }, { name: "foo", replacementSpan: r3 }]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
////export const x = 0;
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
////import {} from "foo//**/";
|
||||
////import {} from "foo/[|/**/|]";
|
||||
|
||||
// @Filename: /tsconfig.json
|
||||
////{
|
||||
@@ -16,4 +16,5 @@
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.completionsAt("", ["a", "b"]);
|
||||
const [replacementSpan] = test.ranges();
|
||||
verify.completionsAt("", [{ name: "a", replacementSpan }, { name: "b", replacementSpan }]);
|
||||
|
||||
@@ -188,7 +188,10 @@ declare namespace FourSlashInterface {
|
||||
class verify extends verifyNegatable {
|
||||
assertHasRanges(ranges: Range[]): void;
|
||||
caretAtMarker(markerName?: string): void;
|
||||
completionsAt(markerName: string, completions: string[], options?: { isNewIdentifierLocation?: boolean }): void;
|
||||
completionsAt(markerName: string, completions: ReadonlyArray<string | { name: string, insertText?: string, replacementSpan?: Range }>, options?: {
|
||||
isNewIdentifierLocation?: boolean;
|
||||
includeInsertTextCompletions?: boolean;
|
||||
}): void;
|
||||
completionsAndDetailsAt(
|
||||
markerName: string,
|
||||
completions: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//// /**
|
||||
//// * @param {string} foo A value.
|
||||
//// * @returns {number} Another value
|
||||
//// * @mytag
|
||||
//// * @mytag
|
||||
//// */
|
||||
//// method4(foo: string) { return 3; }
|
||||
//// }
|
||||
|
||||
Reference in New Issue
Block a user