diff --git a/README.MD b/README.MD index d62669d9..5949d7b7 100644 --- a/README.MD +++ b/README.MD @@ -315,6 +315,12 @@ function Foo() { } ``` +`tsEssentialPlugins.methodSnippetsInsertText`: + +Optionally resolve insertText of all completion at suggest trigger: + +![method-snippets-insert-text](media/method-snippets-insert-text.png) + ### Ambiguous Suggestions Some variables like `Object` or `lodash` are common to access properties as well as call directly: diff --git a/media/method-snippets-insert-text.png b/media/method-snippets-insert-text.png new file mode 100644 index 00000000..a9c2293a Binary files /dev/null and b/media/method-snippets-insert-text.png differ diff --git a/package.json b/package.json index 9c6ba560..2fb94337 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ "command": "insertNameOfCompletion", "title": "Insert Name of Completion", "category": "TS Essentials" + }, + { + "command": "copyFullType", + "title": "Copy Full Type" } ], "keybindings": [ diff --git a/src/configurationType.ts b/src/configurationType.ts index 4b1a4c92..33fbe8b6 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -358,6 +358,15 @@ export type Configuration = { * @default true */ enableMethodSnippets: boolean + /** + * Wether add insert text and detail to every function completion on each suggest trigger (instead of expanding method snippet after completion accept). + * This way you can enable support for method snippets in Vue files. + * `methodSnippets.replaceArguments` isn't supported for now. + * This is not enabled by default as it might be really slow in some cases. + * Recommended to try! + * @default disable + */ + methodSnippetsInsertText: 'disable' | 'only-local' | 'all' /** * ```ts * const example = ({ a }, b?, c = 5, ...d) => { } @@ -540,6 +549,12 @@ export type Configuration = { * @default false */ 'experiments.changeKindToFunction': boolean + /** + * Use workaround method for inserting name of TypeScript suggestion. + * If you move to next suggestion and then to previous, and then run *insert name of completion* via keybinding, name of **last resolved** completion will be inserted, so you might prefer to enable this setting. Also it makes this feature work in Vue. + * @default false + */ + 'experiments.enableInsertNameOfSuggestionFix': boolean /** * Map *symbol - array of modules* to change sorting of imports - first available takes precedence in auto import code fixes (+ import all action) * diff --git a/src/onCompletionAccepted.ts b/src/onCompletionAccepted.ts index 00cd1cef..c29c9e80 100644 --- a/src/onCompletionAccepted.ts +++ b/src/onCompletionAccepted.ts @@ -3,19 +3,19 @@ import { getActiveRegularEditor } from '@zardoy/vscode-utils' import { expandPosition } from '@zardoy/vscode-utils/build/position' import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework' import { oneOf } from '@zardoy/utils' -import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes' -import { sendCommand } from './sendCommand' + +export const onCompletionAcceptedOverride: { value: ((item: any) => void) | undefined } = { value: undefined } export default (tsApi: { onCompletionAccepted }) => { let inFlightMethodSnippetOperation: undefined | AbortController let justAcceptedReturnKeywordSuggestion = false let lastAcceptedAmbiguousMethodSnippetSuggestion: string | undefined - let onCompletionAcceptedOverride: ((item: any) => void) | undefined // eslint-disable-next-line complexity tsApi.onCompletionAccepted(async (item: vscode.CompletionItem & { document: vscode.TextDocument; tsEntry }) => { - if (onCompletionAcceptedOverride) { - onCompletionAcceptedOverride(item) + if (onCompletionAcceptedOverride.value) { + onCompletionAcceptedOverride.value(item) + onCompletionAcceptedOverride.value = undefined return } @@ -38,10 +38,18 @@ export default (tsApi: { onCompletionAccepted }) => { if (/* snippet is by vscode or by us to ignore pos */ typeof insertText !== 'object') { const editor = getActiveRegularEditor()! - if (item.tsEntry.source) { + + const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation + const dataMarker = ''))) + const { methodSnippet: params, isAmbiguous, wordStartOffset } = parsed + const startPos = editor.selection.start + const acceptedWordStartOffset = wordStartOffset !== undefined && editor.document.getWordRangeAtPosition(startPos, /[\w\d]+/i)?.start + if (!oneOf(acceptedWordStartOffset, false, undefined) && wordStartOffset === editor.document.offsetAt(acceptedWordStartOffset)) { await new Promise(resolve => { - vscode.workspace.onDidChangeTextDocument(({ document }) => { - if (editor.document !== document) return + vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => { + if (document !== editor.document || contentChanges.length === 0) return resolve() }) }) @@ -50,12 +58,9 @@ export default (tsApi: { onCompletionAccepted }) => { }) } - const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation - const dataMarker = ''))) - const { methodSnippet: params, isAmbiguous } = parsed - if (!params) return + // nextChar check also duplicated in completionEntryDetails for perf, but we need to run this check again with correct position + const nextChar = editor.document.getText(new vscode.Range(startPos, startPos.translate(0, 1))) + if (!params || ['(', '.', '`'].includes(nextChar)) return if (isAmbiguous && lastAcceptedAmbiguousMethodSnippetSuggestion !== suggestionName) { lastAcceptedAmbiguousMethodSnippetSuggestion = suggestionName @@ -100,10 +105,9 @@ export default (tsApi: { onCompletionAccepted }) => { async (_progress, token) => { const accepted = await new Promise(resolve => { token.onCancellationRequested(() => { - onCompletionAcceptedOverride = undefined resolve(false) }) - onCompletionAcceptedOverride = item => { + onCompletionAcceptedOverride.value = item => { console.dir(item, { depth: 4 }) resolve(true) } diff --git a/src/specialCommands.ts b/src/specialCommands.ts index fde747d2..df878de9 100644 --- a/src/specialCommands.ts +++ b/src/specialCommands.ts @@ -1,12 +1,14 @@ import * as vscode from 'vscode' import { getActiveRegularEditor } from '@zardoy/vscode-utils' -import { getExtensionCommandId, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework' +import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework' import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick' import _ from 'lodash' import { compact } from '@zardoy/utils' +import { offsetPosition } from '@zardoy/vscode-utils/build/position' import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes' import { sendCommand } from './sendCommand' import { tsRangeToVscode, tsRangeToVscodeSelection } from './util' +import { onCompletionAcceptedOverride } from './onCompletionAccepted' export default () => { registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => { @@ -238,11 +240,52 @@ export default () => { await vscode.commands.executeCommand(preview ? 'acceptRenameInputWithPreview' : 'acceptRenameInput') }) - registerExtensionCommand('insertNameOfCompletion', async () => { + registerExtensionCommand('insertNameOfCompletion', async (_, { insertMode } = {}) => { const editor = vscode.window.activeTextEditor if (!editor) return - const result = await sendCommand('getLastResolvedCompletion') - if (!result) return - await editor.insertSnippet(new vscode.SnippetString().appendText(result.name)) + if (!getExtensionSetting('experiments.enableInsertNameOfSuggestionFix')) { + const result = await sendCommand('getLastResolvedCompletion') + if (!result) return + const position = editor.selection.active + const range = result.range ? tsRangeToVscode(editor.document, result.range) : editor.document.getWordRangeAtPosition(position) + await editor.insertSnippet( + new vscode.SnippetString().appendText(result.name), + (insertMode || vscode.workspace.getConfiguration().get('editor.suggest.insertMode')) === 'replace' ? range : range?.with(undefined, position), + ) + return + } + + onCompletionAcceptedOverride.value = () => {} + const { ranges, text } = await new Promise<{ text: string; ranges: vscode.Range[] }>(resolve => { + vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => { + if (document !== editor.document || contentChanges.length === 0) return + const ranges = contentChanges.map( + change => new vscode.Range(change.range.start, offsetPosition(document, change.range.start, change.text.length)), + ) + resolve({ ranges, text: contentChanges[0]!.text }) + }) + void vscode.commands.executeCommand('acceptSelectedSuggestion') + }) + const needle = ['(', ': '].find(needle => text.includes(needle)) + if (!needle) return + const cleanedText = text.slice(0, text.indexOf(needle)) + await editor.edit( + e => { + for (const range of ranges) { + e.replace(range, cleanedText) + } + }, + { + undoStopBefore: false, + undoStopAfter: false, + }, + ) + }) + + registerExtensionCommand('copyFullType', async () => { + const response = await sendCommand('getFullType') + if (!response) return + const { text } = response + await vscode.env.clipboard.writeText(text) }) } diff --git a/typescript/src/completionEntryDetails.ts b/typescript/src/completionEntryDetails.ts index 5e205793..7c8d15fb 100644 --- a/typescript/src/completionEntryDetails.ts +++ b/typescript/src/completionEntryDetails.ts @@ -3,6 +3,7 @@ import constructMethodSnippet from './constructMethodSnippet' import { RequestResponseTypes } from './ipcTypes' import namespaceAutoImports from './namespaceAutoImports' import { GetConfig } from './types' +import { wordStartAtPos } from './utils' export const lastResolvedCompletion = { value: undefined as undefined | RequestResponseTypes['getLastResolvedCompletion'], @@ -16,7 +17,7 @@ export default function completionEntryDetails( { enableMethodCompletion, completionsSymbolMap }: PrevCompletionsAdditionalData, ): ts.CompletionEntryDetails | undefined { const [fileName, position, entryName, formatOptions, source, preferences, data] = inputArgs - lastResolvedCompletion.value = { name: entryName } + lastResolvedCompletion.value = { name: entryName, range: prevCompletionsMap[entryName]?.range } const program = languageService.getProgram() const sourceFile = program?.getSourceFile(fileName) if (!program || !sourceFile) return @@ -49,6 +50,7 @@ export default function completionEntryDetails( prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts] } if (!prior) return + // might be incorrect: write [].entries() -> []|.entries|() -> []./*position*/e const nextChar = sourceFile.getFullText().slice(position, position + 1) if (enableMethodCompletion && c('enableMethodSnippets') && !['(', '.', '`'].includes(nextChar)) { @@ -59,7 +61,12 @@ export default function completionEntryDetails( } const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData) if (methodSnippet) { - const data = JSON.stringify({ methodSnippet, isAmbiguous: resolveData.isAmbiguous }) + const wordStartOffset = source ? wordStartAtPos(sourceFile.getFullText(), position) : undefined + const data = JSON.stringify({ + methodSnippet, + isAmbiguous: resolveData.isAmbiguous, + wordStartOffset, + }) prior.documentation = [{ kind: 'text', text: `` }, ...(prior.documentation ?? [])] } } diff --git a/typescript/src/completions/changeKindToFunction.ts b/typescript/src/completions/changeKindToFunction.ts deleted file mode 100644 index 6ca6d834..00000000 --- a/typescript/src/completions/changeKindToFunction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { oneOf } from '@zardoy/utils' -import { sharedCompletionContext } from './sharedContext' - -export default (entries: ts.CompletionEntry[]) => { - const { languageService } = sharedCompletionContext - - const typeChecker = languageService.getProgram()!.getTypeChecker()! - let timeSpend = 0 - const newEntries = entries.map(entry => { - const patch = (): ts.CompletionEntry | undefined => { - const { kind } = entry - if ( - !oneOf( - kind, - ts.ScriptElementKind.alias, - ts.ScriptElementKind.memberVariableElement, - ts.ScriptElementKind.variableElement, - ts.ScriptElementKind.localVariableElement, - ts.ScriptElementKind.constElement, - ts.ScriptElementKind.variableElement, - ) - ) { - return - } - const { symbol } = entry - if (!symbol) return - const { valueDeclaration } = symbol - if (!valueDeclaration) return - - const dateNow = Date.now() - const type = typeChecker.getTypeOfSymbolAtLocation(symbol, valueDeclaration) - const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call) - timeSpend += Date.now() - dateNow - if (signatures.length === 0) return - - return { ...entry, kind: ts.ScriptElementKind.functionElement } - } - - return patch() ?? entry - }) - - // remove logging once stable - console.log('changeKindToFunction time:', timeSpend) - - return newEntries -} diff --git a/typescript/src/completions/functionCompletions.ts b/typescript/src/completions/functionCompletions.ts new file mode 100644 index 00000000..0d884391 --- /dev/null +++ b/typescript/src/completions/functionCompletions.ts @@ -0,0 +1,70 @@ +import { oneOf } from '@zardoy/utils' +import constructMethodSnippet from '../constructMethodSnippet' +import { insertTextAfterEntry } from '../utils' +import { sharedCompletionContext } from './sharedContext' + +export default (entries: ts.CompletionEntry[]) => { + const { languageService, c, sourceFile, position } = sharedCompletionContext + + const methodSnippetInsertTextMode = c('methodSnippetsInsertText') + const enableResolvingInsertText = c('enableMethodSnippets') && methodSnippetInsertTextMode !== 'disable' + const changeKindToFunction = c('experiments.changeKindToFunction') + + if (!enableResolvingInsertText && !changeKindToFunction) return + + const typeChecker = languageService.getProgram()!.getTypeChecker()! + // let timeSpend = 0 + const newEntries = entries.map(entry => { + const patch = (): ts.CompletionEntry | undefined => { + const { kind, symbol } = entry + if ( + !enableResolvingInsertText && + !oneOf( + kind, + ts.ScriptElementKind.alias, + ts.ScriptElementKind.memberVariableElement, + ts.ScriptElementKind.variableElement, + ts.ScriptElementKind.localVariableElement, + ts.ScriptElementKind.constElement, + ts.ScriptElementKind.variableElement, + ) + ) { + return + } + if (methodSnippetInsertTextMode === 'only-local' && entry.source) return + if (!symbol) return + const { valueDeclaration = symbol.declarations?.[0] } = symbol + if (!valueDeclaration) return + + // const dateNow = Date.now() + if (enableResolvingInsertText) { + const resolveData = {} as { isAmbiguous: boolean } + const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData) + if (!methodSnippet || resolveData.isAmbiguous) return + return { + ...entry, + insertText: insertTextAfterEntry(entry, `(${methodSnippet.map((x, i) => `$\{${i + 1}:${x}}`).join(', ')})`), + labelDetails: { + detail: `(${methodSnippet.join(', ')})`, + description: ts.displayPartsToString(entry.sourceDisplay), + }, + kind: changeKindToFunction ? ts.ScriptElementKind.functionElement : entry.kind, + isSnippet: true, + } + } + const type = typeChecker.getTypeOfSymbolAtLocation(symbol, valueDeclaration) + const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call) + // timeSpend += Date.now() - dateNow + if (signatures.length === 0) return + + return { ...entry, kind: ts.ScriptElementKind.functionElement } + } + + return patch() ?? entry + }) + + // remove logging once stable + // console.log('changeKindToFunction time:', timeSpend) + + return newEntries +} diff --git a/typescript/src/completions/localityBonus.ts b/typescript/src/completions/localityBonus.ts index 3695c11a..cec7e5c1 100644 --- a/typescript/src/completions/localityBonus.ts +++ b/typescript/src/completions/localityBonus.ts @@ -9,7 +9,7 @@ export default (entries: ts.CompletionEntry[]) => { // eslint-disable-next-line prefer-destructuring const symbol: ts.Symbol | undefined = entry['symbol'] if (!symbol) return - const { valueDeclaration } = symbol + const { valueDeclaration = symbol.declarations?.[0] } = symbol if (!valueDeclaration) return if (valueDeclaration.getSourceFile().fileName !== sourceFile.fileName) return -1 return valueDeclaration.pos diff --git a/typescript/src/completions/objectLiteralCompletions.ts b/typescript/src/completions/objectLiteralCompletions.ts index 4e4edf33..f0531292 100644 --- a/typescript/src/completions/objectLiteralCompletions.ts +++ b/typescript/src/completions/objectLiteralCompletions.ts @@ -1,4 +1,4 @@ -import { getFullTypeChecker, isTs5 } from '../utils' +import { getFullTypeChecker, insertTextAfterEntry, isTs5 } from '../utils' import { sharedCompletionContext } from './sharedContext' export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => { @@ -22,8 +22,8 @@ export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => { if (!objType) return oldProperties = getAllPropertiesOfType(objType, typeChecker) } - // eslint-disable-next-line unicorn/no-useless-spread - for (const entry of [...entries]) { + const clonedEntries = [...entries] + for (const entry of clonedEntries) { let type: ts.Type | undefined if (!isTs5()) { const property = oldProperties!.find(property => property.name === entry.name) @@ -66,7 +66,7 @@ export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => { const insertSnippetVariant = completingStyleMap.find(([, detector]) => detector(type!, typeChecker))?.[0] ?? fallbackSnippet if (!insertSnippetVariant) continue const [insertSnippetText, insertSnippetPreview] = typeof insertSnippetVariant === 'function' ? insertSnippetVariant() : insertSnippetVariant - const insertText = entry.name + insertSnippetText + const insertText = insertTextAfterEntry(entry, insertSnippetText) const index = entries.indexOf(entry) entries.splice(index + (keepOriginal === 'before' ? 1 : 0), keepOriginal === 'remove' ? 1 : 0, { ...entry, diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index bb3b0c9d..6e67ee35 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -26,11 +26,11 @@ import adjustAutoImports from './completions/adjustAutoImports' import addSourceDefinition from './completions/addSourceDefinition' import { sharedCompletionContext } from './completions/sharedContext' import displayImportedInfo from './completions/displayImportedInfo' -import changeKindToFunction from './completions/changeKindToFunction' import functionPropsAndMethods from './completions/functionPropsAndMethods' import { getTupleSignature } from './tupleSignature' import stringTemplateTypeCompletions from './completions/stringTemplateType' import localityBonus from './completions/localityBonus' +import functionCompletions from './completions/functionCompletions' export type PrevCompletionMap = Record< string, @@ -40,6 +40,7 @@ export type PrevCompletionMap = Record< documentationOverride?: string | ts.SymbolDisplayPart[] detailPrepend?: string documentationAppend?: string + range?: [number, number] // textChanges?: ts.TextChange[] } > @@ -48,7 +49,7 @@ export type PrevCompletionsAdditionalData = { completionsSymbolMap: Map> } -type GetCompletionAtPositionReturnType = { +export type GetCompletionAtPositionReturnType = { completions: ts.CompletionInfo /** Let default getCompletionEntryDetails to know original name or let add documentation from here */ prevCompletionsMap: PrevCompletionMap @@ -356,9 +357,7 @@ export const getCompletionsAtPosition = ( if (exactNode) { prior.entries = filterJsxElements(prior.entries, exactNode, position, languageService, c) ?? prior.entries } - if (c('experiments.changeKindToFunction')) { - prior.entries = changeKindToFunction(prior.entries) - } + prior.entries = functionCompletions(prior.entries) ?? prior.entries if (c('correctSorting.enable')) { prior.entries = prior.entries.map(({ ...entry }, index) => ({ @@ -382,6 +381,13 @@ export const getCompletionsAtPosition = ( } } + for (const entry of prior.entries) { + const { replacementSpan } = entry + if (!replacementSpan) continue + prevCompletionsMap[entry.name] ??= {} + prevCompletionsMap[entry.name]!.range = [replacementSpan.start, ts.textSpanEnd(replacementSpan)] + } + // Otherwise may crash Volar prior.entries = prior.entries.map(entry => ({ ...entry, diff --git a/typescript/src/ipcTypes.ts b/typescript/src/ipcTypes.ts index 9a144178..ced0bef0 100644 --- a/typescript/src/ipcTypes.ts +++ b/typescript/src/ipcTypes.ts @@ -1,6 +1,6 @@ // should-not contain other typescript/* imports that use globals as is imported in extension code (src/) -export const passthroughExposedApiCommands = ['getNodePath', 'getSpanOfEnclosingComment', 'getNodeAtPosition'] as const +export const passthroughExposedApiCommands = ['getNodePath', 'getSpanOfEnclosingComment', 'getNodeAtPosition', 'getFullType'] as const export const triggerCharacterCommands = [ ...passthroughExposedApiCommands, @@ -80,6 +80,10 @@ export type RequestResponseTypes = { getExtendedCodeActionEdits: ApplyExtendedCodeActionResult getLastResolvedCompletion: { name: string + range?: TsRange + } + getFullType: { + text: string } } diff --git a/typescript/src/specialCommands/handle.ts b/typescript/src/specialCommands/handle.ts index a3da1ca6..e1261327 100644 --- a/typescript/src/specialCommands/handle.ts +++ b/typescript/src/specialCommands/handle.ts @@ -254,6 +254,15 @@ export default ( if (specialCommand === 'getLastResolvedCompletion') { return lastResolvedCompletion.value } + if (specialCommand === 'getFullType') { + const node = findChildContainingExactPosition(sourceFile, position) + if (!node) return + const checker = languageService.getProgram()!.getTypeChecker()! + const type = checker.getTypeAtLocation(node) + return { + text: checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.NoTypeReduction), + } + } return null } diff --git a/typescript/src/tupleSignature.ts b/typescript/src/tupleSignature.ts index dedb1c10..42715980 100644 --- a/typescript/src/tupleSignature.ts +++ b/typescript/src/tupleSignature.ts @@ -49,7 +49,11 @@ export const getTupleSignature = (node: ts.Node, typeChecker: ts.TypeChecker) => if (!/^\d+$/.test(property.name)) return const type = typeChecker.getTypeOfSymbolAtLocation(property, targetNode!) let displayString = typeChecker.typeToString(type) - const tupleLabelDeclaration: ts.NamedTupleMember | undefined = property['target']?.['tupleLabelDeclaration'] + const tupleLabelDeclaration: ts.NamedTupleMember | undefined = + property['target']?.['tupleLabelDeclaration'] ?? + // https://github.com/microsoft/TypeScript/blob/main/src/services/symbolDisplay.ts#L648 (labelDecl) + // todo uncomment after ts-expose-internals update to 5.0 + (property as any) /*import('typescript-full').TransientSymbol*/?.links?.target?.links?.tupleLabelDeclaration const tupleLabel = tupleLabelDeclaration?.name.text if (tupleLabel) { displayString = `${tupleLabel}: ${displayString}` diff --git a/typescript/src/utils.ts b/typescript/src/utils.ts index a2f23152..f9726006 100644 --- a/typescript/src/utils.ts +++ b/typescript/src/utils.ts @@ -258,6 +258,17 @@ const wordRangeAtPos = (text: string, position: number) => { return text.slice(startPos + 1, endPos) } +export const wordStartAtPos = (text: string, position: number) => { + const isGood = (pos: number) => { + return /[\w\d]/i.test(text.at(pos - 1) ?? '') + } + let startPos = position + while (isGood(startPos)) { + startPos-- + } + return startPos +} + type GetIs = T extends (elem: any) => elem is infer T ? T : never export const createDummySourceFile = (code: string) => { @@ -281,3 +292,6 @@ export const patchMethod = (obj: T, method: K, overriden: }) } } + +export const insertTextAfterEntry = (entryOrName: ts.CompletionEntry | string, appendText: string) => + (typeof entryOrName === 'string' ? entryOrName : entryOrName.name).replace(/\$/g, '\\$') + appendText diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index dc25f961..9f2b1302 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -534,6 +534,17 @@ test('Additional types suggestions', () => { }) }) +test('Tuple signature', () => { + const tester = fourslashLikeTester(/* ts */ ` + const [/*1*/] = [] as [a: number] + `) + tester.completion(1, { + exact: { + names: ['a'], + }, + }) +}) + test('Object Literal Completions', () => { const [_positivePositions, _negativePositions, numPositions] = fileContentsSpecialPositions(/* ts */ ` interface Options { @@ -684,7 +695,10 @@ test('In Keyword Completions', () => { const completion = pickObj(getCompletionsAtPosition(pos!, { shouldHave: true })!, 'entriesSorted', 'prevCompletionsMap') // this test is bad case of demonstrating how it can be used with string in union (IT SHOULDNT!) // but it is here to ensure this is no previous crash issue, indexes are correct when used only with objects - expect(completion).toMatchInlineSnapshot(` + expect({ + ...completion, + prevCompletionsMap: Object.entries(completion.prevCompletionsMap).map(([key, v]) => [key, (v.documentationOverride as string).replaceAll('\n', ' ')]), + }).toMatchInlineSnapshot(` { "entriesSorted": [ { @@ -727,19 +741,20 @@ test('In Keyword Completions', () => { }, }, ], - "prevCompletionsMap": { - "a": { - "documentationOverride": "2: boolean - - 3: number", - }, - "☆b": { - "documentationOverride": "2: string", - }, - "☆c": { - "documentationOverride": "3: number", - }, - }, + "prevCompletionsMap": [ + [ + "a", + "2: boolean 3: number", + ], + [ + "☆b", + "2: string", + ], + [ + "☆c", + "3: number", + ], + ], } `) }) diff --git a/typescript/test/shared.ts b/typescript/test/shared.ts index 24df2189..59b57111 100644 --- a/typescript/test/shared.ts +++ b/typescript/test/shared.ts @@ -1,5 +1,6 @@ +/* eslint-disable import/first */ beforeAll(() => { - //@ts-ignore plugin expect it to set globallly + //@ts-expect-error plugin expect it to set globallly globalThis.__WEB__ = false }) @@ -15,5 +16,6 @@ export const settingsOverride: Partial = { 'arrayMethodsSnippets.enable': true, 'codeActions.extractTypeInferName': true, 'methodSnippets.skip': 'no-skip', + tupleHelpSignature: true, } export const defaultConfigFunc = await getDefaultConfigFunc(settingsOverride) diff --git a/typescript/test/testing.ts b/typescript/test/testing.ts index 0f8afa8e..c0db7325 100644 --- a/typescript/test/testing.ts +++ b/typescript/test/testing.ts @@ -64,17 +64,16 @@ export const overrideSettings = (newOverrides: Partial) => { export const fourslashLikeTester = (contents: string, fileName = entrypoint) => { const [positive, _negative, numberedPositions] = fileContentsSpecialPositions(contents, fileName) - const ranges = positive.reduce( + const ranges = positive.reduce( (prevRanges, pos) => { const lastPrev = prevRanges[prevRanges.length - 1]! if (lastPrev.length < 2) { lastPrev.push(pos) return prevRanges - } else { - return [...prevRanges, [pos]] } + return [...prevRanges, [pos]] }, - [[]] as number[][], + [[]], ) return { completion: (marker: number | number[], matcher: CompletionMatcher, meta?) => { @@ -127,7 +126,7 @@ export const fourslashLikeTester = (contents: string, fileName = entrypoint) => )! const action = actionsGroup.actions.find(action => action.description === refactorName)! const { edits } = languageService.getEditsForRefactor(fileName, {}, { pos: start, end }, actionsGroup.name, action.name, {})! - const a = tsFull.textChanges.applyChanges(getCurrentFile(), edits![0]!.textChanges) + const a = tsFull.textChanges.applyChanges(getCurrentFile(), edits[0]!.textChanges) } else { expect(appliableNames, `at marker ${mark}`).not.toContain(refactorName) } @@ -150,7 +149,7 @@ export const fileContentsSpecialPositions = (contents: string, fileName = entryp let mainMatch = currentMatch[1]! if (addOnly) mainMatch = mainMatch.slice(0, -1) const possiblyNum = +mainMatch - if (!isNaN(possiblyNum)) { + if (!Number.isNaN(possiblyNum)) { addArr[2][possiblyNum] = offset } else { addArr[mainMatch === 't' ? '0' : '1'].push(offset)