diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index df103e9dafc33..6a2f71df10e62 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -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, 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, 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; } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 34c8f9e9656f8..10c5aad92db2b 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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`); }); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index ef59cc164069f..b3cbba27a2067 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -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. diff --git a/src/server/session.ts b/src/server/session.ts index 9dcf58734a73d..693d1651c911d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1207,10 +1207,10 @@ namespace ts.server { if (simplifiedResult) { return mapDefined(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)); } diff --git a/src/services/completions.ts b/src/services/completions.ts index b92dffb93cc32..bdd5e4a86a217 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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 =
// The completion list at "1" will contain "div" with type any - const tagName = (location.parent.parent).openingElement.tagName; + const tagName = location.parent.parent.openingElement.tagName; return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: [{ - name: (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, target: ScriptTarget, entries: Push): 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, entries: Push, 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 { @@ -206,7 +232,7 @@ namespace ts.Completions { const uniques = createMap(); 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(node.parent, typeChecker, compilerOptions.target, log); + return getStringLiteralCompletionEntriesFromPropertyAssignment(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((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, - ): { 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(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, - ): { 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 + ): 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; - 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; + 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, 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 = (contextToken.parent).expression; - isRightOfDot = true; - } - else if (parent.kind === SyntaxKind.QualifiedName) { - node = (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 = (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), (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; @@ -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); } } diff --git a/src/services/services.ts b/src/services/services.ts index 94862980ca7d2..eb5e3ae574379 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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, diff --git a/src/services/types.ts b/src/services/types.ts index 8e6704866495f..597709a7c7919 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -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. diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 116bedfabb7c2..b0dc9e3b6eb33 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -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. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 8a01bc3a89612..f839deabcfbe8 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -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. diff --git a/tests/cases/fourslash/completionListInvalidMemberNames.ts b/tests/cases/fourslash/completionListInvalidMemberNames.ts index 8e62ba2fb67b9..3dc54274e838e 100644 --- a/tests/cases/fourslash/completionListInvalidMemberNames.ts +++ b/tests/cases/fourslash/completionListInvalidMemberNames.ts @@ -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, +}); diff --git a/tests/cases/fourslash/completionListInvalidMemberNames2.ts b/tests/cases/fourslash/completionListInvalidMemberNames2.ts index 753f9bbcb3064..153ed3bdb5e32 100644 --- a/tests/cases/fourslash/completionListInvalidMemberNames2.ts +++ b/tests/cases/fourslash/completionListInvalidMemberNames2.ts @@ -1,10 +1,19 @@ /// -////enum Foo { -//// X, Y, '☆' +// TODO: we should probably support this like we do in completionListInvalidMemberNames.ts + +////declare var Symbol: SymbolConstructor; +////interface SymbolConstructor { +//// readonly hasInstance: symbol; +////} +////interface Function { +//// [Symbol.hasInstance](value: any): boolean; +////} +////interface SomeInterface { +//// (value: number): any; ////} -////Foo./*a*/; -////Foo["/*b*/"]; +////var _ : SomeInterface; +////_./**/ -verify.completionsAt("a", ["X", "Y"]); -verify.completionsAt("b", ["X", "Y", "☆"]); +goTo.marker(); +verify.not.completionListContains("[Symbol.hasInstance]"); diff --git a/tests/cases/fourslash/completionListInvalidMemberNames3.ts b/tests/cases/fourslash/completionListInvalidMemberNames3.ts deleted file mode 100644 index cf1141b409409..0000000000000 --- a/tests/cases/fourslash/completionListInvalidMemberNames3.ts +++ /dev/null @@ -1,71 +0,0 @@ -/// - -// @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;/// - -////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]"); \ No newline at end of file diff --git a/tests/cases/fourslash/completionListInvalidMemberNames_startWithSpace.ts b/tests/cases/fourslash/completionListInvalidMemberNames_startWithSpace.ts new file mode 100644 index 0000000000000..f86c8a5a8132b --- /dev/null +++ b/tests/cases/fourslash/completionListInvalidMemberNames_startWithSpace.ts @@ -0,0 +1,8 @@ +/// + +////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 }); diff --git a/tests/cases/fourslash/completionListInvalidMemberNames_withExistingIdentifier.ts b/tests/cases/fourslash/completionListInvalidMemberNames_withExistingIdentifier.ts new file mode 100644 index 0000000000000..8463a710b01ae --- /dev/null +++ b/tests/cases/fourslash/completionListInvalidMemberNames_withExistingIdentifier.ts @@ -0,0 +1,7 @@ +/// + +////declare const x: { "foo ": "space in the name", }; +////x[|.fo/**/|]; + +const replacementSpan = test.ranges()[0]; +verify.completionsAt("", [{ name: "foo ", insertText: '["foo "]', replacementSpan }], { includeInsertTextCompletions: true }); diff --git a/tests/cases/fourslash/completion_enum-members-with-invalid-identifiers-should-not-show-in-completion.ts b/tests/cases/fourslash/completion_enum-members-with-invalid-identifiers-should-not-show-in-completion.ts deleted file mode 100644 index 6d5b316719838..0000000000000 --- a/tests/cases/fourslash/completion_enum-members-with-invalid-identifiers-should-not-show-in-completion.ts +++ /dev/null @@ -1,13 +0,0 @@ -/// - -//// enum e { -//// "1", -//// 2 = 3, -//// 3, -//// a, -//// b -//// } -//// -//// e./**/ - -verify.completionsAt("", ["a", "b"]); diff --git a/tests/cases/fourslash/completionsPaths.ts b/tests/cases/fourslash/completionsPaths.ts index 454ef78eef830..6bb2fb3870635 100644 --- a/tests/cases/fourslash/completionsPaths.ts +++ b/tests/cases/fourslash/completionsPaths.ts @@ -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 }]); diff --git a/tests/cases/fourslash/completionsPaths_pathMapping.ts b/tests/cases/fourslash/completionsPaths_pathMapping.ts index 61ea3d5d9367c..b2ec9ac61988e 100644 --- a/tests/cases/fourslash/completionsPaths_pathMapping.ts +++ b/tests/cases/fourslash/completionsPaths_pathMapping.ts @@ -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 }]); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index ec2a5d1a2ea37..426518794bd7c 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -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, options?: { + isNewIdentifierLocation?: boolean; + includeInsertTextCompletions?: boolean; + }): void; completionsAndDetailsAt( markerName: string, completions: { diff --git a/tests/cases/fourslash/jsdocTypedefTagTypeExpressionCompletion.ts b/tests/cases/fourslash/jsdocTypedefTagTypeExpressionCompletion.ts index 379cc60e0587b..b7a2b7d899397 100644 --- a/tests/cases/fourslash/jsdocTypedefTagTypeExpressionCompletion.ts +++ b/tests/cases/fourslash/jsdocTypedefTagTypeExpressionCompletion.ts @@ -11,7 +11,7 @@ //// /** //// * @param {string} foo A value. //// * @returns {number} Another value -//// * @mytag +//// * @mytag //// */ //// method4(foo: string) { return 3; } //// }