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:
Andy
2018-01-08 18:57:46 -08:00
committed by GitHub
parent 73e3e8d790
commit 89ceb4b9b5
19 changed files with 344 additions and 291 deletions

View File

@@ -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;
}

View File

@@ -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`);
});

View File

@@ -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.

View File

@@ -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));
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
});

View File

@@ -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]");

View File

@@ -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
//// * constructors 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]");

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -1,13 +0,0 @@
/// <reference path='fourslash.ts'/>
//// enum e {
//// "1",
//// 2 = 3,
//// 3,
//// a,
//// b
//// }
////
//// e./**/
verify.completionsAt("", ["a", "b"]);

View File

@@ -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 }]);

View File

@@ -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 }]);

View File

@@ -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: {

View File

@@ -11,7 +11,7 @@
//// /**
//// * @param {string} foo A value.
//// * @returns {number} Another value
//// * @mytag
//// * @mytag
//// */
//// method4(foo: string) { return 3; }
//// }