diff --git a/.vscode/settings.json b/.vscode/settings.json index fc58dc3..e433fea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,10 @@ "typescript/lib/tsserverlibrary": { "namespace": "ts", "addImport": false + }, + "typescript": { + "namespace": "ts", + "addImport": false } }, "typescript.preferences.autoImportFileExcludePatterns": [ @@ -24,5 +28,5 @@ "vitest.showFailMessages": true, "vitest.include": [ "typescript/test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}" - ] + ], } diff --git a/package.json b/package.json index 9e9f1c4..26a9f14 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,10 @@ { "command": "migrateRequireToImports", "title": "Migrate Require to Imports" + }, + { + "command": "wrapIntoNewTag", + "title": "Wrap Into New Tag" } ], "keybindings": [ @@ -169,7 +173,7 @@ "@vue/language-server": "latest", "@vue/language-service": "latest", "@zardoy/utils": "^0.0.9", - "@zardoy/vscode-utils": "^0.0.47", + "@zardoy/vscode-utils": "^0.0.52", "chai": "^4.3.6", "change-case": "^4.1.2", "chokidar": "^3.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39c742e..d0991fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^0.0.9 version: 0.0.9 '@zardoy/vscode-utils': - specifier: ^0.0.47 - version: 0.0.47(@types/vscode@1.72.0)(esbuild@0.15.18)(vscode-framework@0.0.18) + specifier: ^0.0.52 + version: 0.0.52(@types/vscode@1.72.0)(esbuild@0.15.18)(vscode-framework@0.0.18) chai: specifier: ^4.3.6 version: 4.3.6 @@ -1090,8 +1090,8 @@ packages: type-fest: 2.19.0 dev: false - /@zardoy/vscode-utils@0.0.47(@types/vscode@1.72.0)(esbuild@0.15.18)(vscode-framework@0.0.18): - resolution: {integrity: sha512-0xsdTonXFxcBdsHcWgkSfJSpP9VOvOiT6oo/LuA5FOGJJI/5W6e6EDwN6Y9FahRWdrYmOfliekEMGZGk/eEWcQ==} + /@zardoy/vscode-utils@0.0.52(@types/vscode@1.72.0)(esbuild@0.15.18)(vscode-framework@0.0.18): + resolution: {integrity: sha512-e+HvIdzALhuFtnub6jzNARge78GPnYuW/+b1jMO0p7A/LjjYCPhFIPjhj/1tf5SHtpbiy06mDb6z8HpGGUhFpg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true peerDependencies: @@ -1126,7 +1126,7 @@ packages: untildify: 4.0.0 vscode-framework: 0.0.18(@types/vscode@1.72.0)(typescript@5.3.3) vscode-manifest: 0.0.8 - vscode-uri: 3.0.6 + vscode-uri: 3.0.8 dev: false /accepts@1.3.7: diff --git a/src/configurationType.ts b/src/configurationType.ts index eb1b98b..93ae2d6 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -680,6 +680,34 @@ export type Configuration = { // bigFilesThreshold: number /** @default false */ enableHooksFile: boolean + /** + * @default false + */ + declareMissingPropertyQuickfixOtherFiles: boolean + /** + * @default {} + */ + filesAutoImport: { + [ext: string]: { + /** + * Override import path (default is "$path") + */ + importPath?: string + /** + * Start phrase that will trigger search for available files import + */ + prefix: string + /** + * @default camel + */ + nameCasing?: 'camel' | 'pascal' | 'constant' | 'snake' + /** + * @default $name + */ + nameTransform?: string + iconPost?: string + } + } } // scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+" diff --git a/src/specialCommands.ts b/src/specialCommands.ts index 9b4d9dd..0e8a55a 100644 --- a/src/specialCommands.ts +++ b/src/specialCommands.ts @@ -1,10 +1,12 @@ import * as vscode from 'vscode' import { getActiveRegularEditor } from '@zardoy/vscode-utils' import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework' +import { registerTextEditorCommand } from '@zardoy/vscode-utils/build/commands' 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 { defaultJsSupersetLangs } from '@zardoy/vscode-utils/build/langs' import { RequestInputTypes, RequestOutputTypes } from '../typescript/src/ipcTypes' import { sendCommand } from './sendCommand' import { tsRangeToVscode, tsRangeToVscodeSelection, tsTextChangesToVscodeTextEdits } from './util' @@ -319,6 +321,37 @@ export default () => { await vscode.workspace.applyEdit(edit) }) + registerTextEditorCommand('wrapIntoNewTag', async (editor, edit, fallbackCommand = 'editor.emmet.action.wrapWithAbbreviation') => { + const { selection } = editor + if (selection.start.isEqual(selection.end)) { + const range = editor.selection + const selectedText = '$TM_SELECTED_TEXT' + + if (!defaultJsSupersetLangs.includes(editor.document.languageId)) { + if (fallbackCommand) { + await vscode.commands.executeCommand(fallbackCommand, ...fallbackCommand.split(' ').slice(1)) + } + + return + } + + const data = (await sendCommand('getNodePath', {})) ?? [] + const jsxElem = [...data].reverse().find(d => ['JsxElement', 'JsxSelfClosingElement', 'JsxFragment'].includes(d.kindName)) + if (!jsxElem) return + const { start, end } = jsxElem + const startPos = editor.document.positionAt(start) + const startRange = new vscode.Range(startPos, startPos) + const endPos = editor.document.positionAt(end) + const endRange = new vscode.Range(endPos, endPos) + editor.selection = new vscode.Selection(startRange.start, endRange.end) + } + + const line = editor.document.lineAt(editor.selection.start.line) + const currentIndent = line.text.slice(0, line.firstNonWhitespaceCharacterIndex) + await editor.insertSnippet(new vscode.SnippetString(`<\${1:div}$0>\n\t$TM_SELECTED_TEXT\n`), editor.selection) + return + }) + // registerExtensionCommand('insertImportFlatten', () => { // // got -> default, got // type A = ts.Type diff --git a/typescript/src/codeActions/custom/addDestructure/addSplittedDestructure.ts b/typescript/src/codeActions/custom/addDestructure/addSplittedDestructure.ts index 318ff94..3021d68 100644 --- a/typescript/src/codeActions/custom/addDestructure/addSplittedDestructure.ts +++ b/typescript/src/codeActions/custom/addDestructure/addSplittedDestructure.ts @@ -1,3 +1,4 @@ +import { uniq } from 'rambda' import { findChildContainingExactPosition, getChangesTracker, getPositionHighlights, isValidInitializerForDestructure, makeUniqueName } from '../../../utils' export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.FormatCodeSettings | undefined, languageService: ts.LanguageService) => { @@ -56,7 +57,7 @@ export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.Form if (!nodeToReplaceWithBindingPattern || propertyNames.length === 0) return - const bindings = propertyNames.map(({ initial, unique }) => { + const bindings = uniq(propertyNames).map(({ initial, unique }) => { return ts.factory.createBindingElement(undefined, unique ? initial : undefined, unique ?? initial) }) diff --git a/typescript/src/codeActions/extended/declareMissingProperties.ts b/typescript/src/codeActions/extended/declareMissingProperties.ts index 5c53bef..5471ad7 100644 --- a/typescript/src/codeActions/extended/declareMissingProperties.ts +++ b/typescript/src/codeActions/extended/declareMissingProperties.ts @@ -5,8 +5,52 @@ export default { codes: [2339], kind: 'quickfix', title: 'Declare missing property', - tryToApply({ sourceFile, node }) { + tryToApply({ sourceFile, node, c, languageService }) { const param = matchParents(node, ['Identifier', 'BindingElement', 'ObjectBindingPattern', 'Parameter']) + const objAccess = matchParents(node, ['Identifier', 'PropertyAccessExpression']) + const missingPropName = (node as ts.Identifier).text + if (objAccess) { + const checker = languageService.getProgram()!.getTypeChecker()! + const type = checker.getContextualType(objAccess.expression) || checker.getTypeAtLocation(objAccess.expression) + const props = type + .getProperties() + .map(type => { + const node = type.declarations?.find(declaration => { + return c('declareMissingPropertyQuickfixOtherFiles') || declaration.getSourceFile().fileName === sourceFile.fileName + }) + if (node === undefined) return undefined! + return { name: type.name, node } + }) + .filter(Boolean) + // TARGET PROP + const propInsertAfter = props.find(prop => missingPropName.startsWith(prop.name)) ?? props.at(-1) + if (propInsertAfter) { + const propInsertParent = propInsertAfter.node.parent + const sameParentLiteralProps = props.filter( + prop => prop.node.parent === propInsertParent && ts.isPropertyAssignment(prop.node) && !ts.isIdentifier(prop.node.initializer), + ) + const insertObject = + sameParentLiteralProps.length > 0 && + sameParentLiteralProps.every(sameParentProp => ts.isObjectLiteralExpression((sameParentProp.node as ts.PropertyAssignment).initializer)) + const insertPos = propInsertAfter.node.end + const insertComma = sourceFile.getFullText().slice(insertPos - 1, insertPos) !== ',' + const getLine = pos => sourceFile.getLineAndCharacterOfPosition(pos).line + const insertNewLine = getLine(propInsertAfter.node.pos) !== getLine(propInsertAfter.node.end) + const insertText = `${insertComma ? ',' : ''}${insertNewLine ? '\n' : ' '}${missingPropName}` + const snippet = insertObject ? `: {${insertNewLine ? '\n\t' : ''}$0${insertNewLine ? '\n' : ''}}` : `$0` + return { + snippetEdits: [ + { + newText: `${tsFull.escapeSnippetText(insertText)}${snippet}`, + span: { + length: 0, + start: insertPos, + }, + }, + ], + } + } + } if (param) { // special react pattern if (ts.isArrowFunction(param.parent) && ts.isVariableDeclaration(param.parent.parent)) { @@ -20,7 +64,7 @@ export default { const hasMembers = param.type.members.length > 0 const insertPos = param.type.members.at(-1)?.end ?? param.type.end - 1 const insertComma = hasMembers && sourceFile.getFullText().slice(insertPos - 1, insertPos) !== ',' - let insertText = (node as ts.Identifier).text + let insertText = missingPropName if (insertComma) insertText = `, ${insertText}` // alternatively only one snippetEdit could be used with tsFull.escapeSnippetText(insertText) + $0 return { diff --git a/typescript/src/codeActions/getCodeActions.ts b/typescript/src/codeActions/getCodeActions.ts index 8ce4fc2..c6ffe52 100644 --- a/typescript/src/codeActions/getCodeActions.ts +++ b/typescript/src/codeActions/getCodeActions.ts @@ -2,6 +2,7 @@ import { compact } from '@zardoy/utils' import { Except } from 'type-fest' import { findChildContainingExactPosition, findChildContainingPosition } from '../utils' import { ApplyExtendedCodeActionResult, IpcExtendedCodeAction } from '../ipcTypes' +import { GetConfig } from '../types' import objectSwapKeysAndValues from './custom/objectSwapKeysAndValues' import changeStringReplaceToRegex from './custom/changeStringReplaceToRegex' import splitDeclarationAndInitialization from './custom/splitDeclarationAndInitialization' @@ -56,6 +57,7 @@ export type ApplyExtendedCodeAction = (options: { /** undefined when no edits is requested */ formatOptions: ts.FormatCodeSettings | undefined languageService: ts.LanguageService + c: GetConfig // languageServiceHost: ts.LanguageServiceHost }) => ApplyExtendedCodeActionResult | boolean | undefined @@ -80,6 +82,7 @@ export const getExtendedCodeActions = ( // languageServiceHost: ts.LanguageServiceHost, formatOptions: ts.FormatCodeSettings | undefined, applyCodeActionTitle: T, + config: GetConfig, filterErrorCodes?: number[], ): T extends undefined ? ExtendedCodeAction[] : ApplyExtendedCodeActionResult => { const range = typeof positionOrRange !== 'number' && positionOrRange.pos !== positionOrRange.end ? positionOrRange : undefined @@ -93,6 +96,7 @@ export const getExtendedCodeActions = ( position, range, sourceFile, + c: config, } if (applyCodeActionTitle) { const codeAction = extendedCodeActions.find(codeAction => codeAction.title === applyCodeActionTitle) diff --git a/typescript/src/completionEntryDetails.ts b/typescript/src/completionEntryDetails.ts index 92e28e4..10f6c5b 100644 --- a/typescript/src/completionEntryDetails.ts +++ b/typescript/src/completionEntryDetails.ts @@ -22,14 +22,29 @@ export default function completionEntryDetails( const sourceFile = program?.getSourceFile(fileName) if (!program || !sourceFile) return - const { documentationOverride, documentationAppend, detailPrepend } = prevCompletionsMap[entryName] ?? {} + const { documentationOverride, documentationAppend, detailPrepend, textChanges } = prevCompletionsMap[entryName] ?? {} if (documentationOverride) { - return { + const prior: ts.CompletionEntryDetails = { name: entryName, kind: ts.ScriptElementKind.alias, kindModifiers: '', displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride, } + if (textChanges) { + prior.codeActions = [ + // ...(prior.codeActions ?? []), + { + description: 'Includes Text Changes', + changes: [ + { + fileName, + textChanges, + }, + ], + }, + ] + } + return prior } let prior = languageService.getCompletionEntryDetails( fileName, diff --git a/typescript/src/completions/filesAutoImport.ts b/typescript/src/completions/filesAutoImport.ts new file mode 100644 index 0000000..a171aba --- /dev/null +++ b/typescript/src/completions/filesAutoImport.ts @@ -0,0 +1,146 @@ +import { camelCase, pascalCase, snakeCase, constantCase } from 'change-case' +import { Configuration } from '../types' +import { nodeModules } from '../utils' +import { sharedCompletionContext } from './sharedContext' + +export default () => { + const { c, prior, languageService, languageServiceHost, node, sourceFile, prevCompletionsMap } = sharedCompletionContext + // todo better web support? + if (!node || !languageServiceHost.readDirectory || !nodeModules?.path) return + const filesAutoImport = c('filesAutoImport') + const included: Array<{ ext: string; item: Configuration['filesAutoImport'][string] }> = [] + const currentText = node.getText() + for (const [ext, item] of Object.entries(filesAutoImport)) { + if (currentText.startsWith(item.prefix)) included.push({ ext, item }) + } + // if (!included.length) return + const root = languageServiceHost.getCurrentDirectory() + // const fileRelative = nodeModules.path.relative(root, sourceFile.fileName) + const collected = [] as string[] + const MAX_ITERATIONS = 200 + let iter = 0 + const collectFiles = (dir: string) => { + iter++ + if (iter > MAX_ITERATIONS) { + console.error('[essentials plugin filesAutoImport] Max iterations reached') + return + } + const files = nodeModules!.fs.readdirSync(dir, { withFileTypes: true }) + for (const file of files) { + if (file.isDirectory()) { + if ( + file.name === 'node_modules' || + file.name.startsWith('.') || + file.name.startsWith('out') || + file.name.startsWith('build') || + file.name.startsWith('dist') + ) + continue + collectFiles(nodeModules!.path.join(dir, file.name)) + } else if (file.isFile()) { + // const ext = nodeModules!.path.extname(file.name) + // if (included.some(i => i.ext === ext)) files.push(nodeModules!.path.join(dir, file.name)) + collected.push(nodeModules!.path.relative(root, nodeModules!.path.join(dir, file.name))) + } + } + } + collectFiles(root) + + const lastImport = sourceFile.statements.filter(ts.isImportDeclaration).at(-1) + + // const directory = languageServiceHost.readDirectory(root, undefined, undefined, undefined, 1) + const completions: Array<{ + name: string + insertText: string + addImport: string + detail: string + description: string + sort: number + }> = [] + for (const { ext, item } of included) { + const files = collected.filter(f => f.endsWith(ext)) + for (const file of files) { + const fullPath = nodeModules.path.join(root, file) + const relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/') + const lastModified = nodeModules.fs.statSync(fullPath).mtime + const lastModifiedFormatted = timeDifference(Date.now(), lastModified.getTime()) + const importPath = (item.importPath ?? '$path').replaceAll('$path', relativeToFile) + const casingFn = { + camel: camelCase, + pascal: pascalCase, + snake: snakeCase, + constant: constantCase, + } + const name = + item.prefix + casingFn[item.nameCasing ?? 'camel']((item.nameTransform ?? '$name').replaceAll('$name', nodeModules.path.basename(file, ext))) + if (prior.entries.some(e => e.name === name)) continue + completions.push({ + name, + insertText: name, + sort: Date.now() - lastModified.getTime(), + detail: `${item.iconPost?.replaceAll('$path', relativeToFile) ?? '📄'} ${lastModifiedFormatted}`, + description: importPath, + addImport: `import ${name} from '${importPath}'`, + }) + } + } + + const prependImport = lastImport ? '\n' : '' + const entries = completions.map(({ name, insertText, detail, sort, addImport, description }): ts.CompletionEntry => { + prevCompletionsMap[name] = { + textChanges: [ + { + newText: `${prependImport}${addImport}`, + span: { + start: lastImport?.end ?? 0, + length: 0, + }, + }, + ], + documentationOverride: description, + } + return { + kind: ts.ScriptElementKind.variableElement, + name, + insertText, + sortText: `${sort}`, + labelDetails: { + description: detail, + }, + // description, + } + }) + return entries +} + +function timeDifference(current, previous) { + const msPerMinute = 60 * 1000 + const msPerHour = msPerMinute * 60 + const msPerDay = msPerHour * 24 + const msPerMonth = msPerDay * 30 + const msPerYear = msPerDay * 365 + + const elapsed = current - previous + + if (elapsed < msPerMinute) { + return `${Math.round(elapsed / 1000)} sec ago` + } + + if (elapsed < msPerHour) { + return `${Math.round(elapsed / msPerMinute)} min ago` + } + + if (elapsed < msPerDay) { + return `${Math.round(elapsed / msPerHour)} h ago` + } + + if (elapsed < msPerMonth) { + return `${Math.round(elapsed / msPerDay)} days ago` + } + + if (elapsed < msPerYear) { + return `${Math.round(elapsed / msPerMonth)} months ago` + } + + return `${Math.round(elapsed / msPerYear)} years ago` +} diff --git a/typescript/src/completions/sharedContext.ts b/typescript/src/completions/sharedContext.ts index 8ac0a46..4e7f67e 100644 --- a/typescript/src/completions/sharedContext.ts +++ b/typescript/src/completions/sharedContext.ts @@ -16,5 +16,5 @@ export const sharedCompletionContext = {} as unknown as Readonly<{ preferences: ts.UserPreferences fullText: string typeChecker: ts.TypeChecker - // languageServiceHost: ts.LanguageServiceHost + languageServiceHost: ts.LanguageServiceHost }> diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index 40b30cf..4beb54b 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -32,6 +32,7 @@ import localityBonus from './completions/localityBonus' import functionCompletions from './completions/functionCompletions' import staticHintSuggestions from './completions/staticHintSuggestions' import typecastCompletions from './completions/typecastCompletions' +import filesAutoImport from './completions/filesAutoImport' export type PrevCompletionMap = Record< string, @@ -42,7 +43,7 @@ export type PrevCompletionMap = Record< detailPrepend?: string documentationAppend?: string range?: [number, number] - // textChanges?: ts.TextChange[] + textChanges?: ts.TextChange[] } > export type PrevCompletionsAdditionalData = { @@ -63,6 +64,7 @@ export const getCompletionsAtPosition = ( options: ts.GetCompletionsAtPositionOptions | undefined, c: GetConfig, languageService: ts.LanguageService, + languageServiceHost: ts.LanguageServiceHost, scriptSnapshot: ts.IScriptSnapshot, formatOptions: ts.FormatCodeSettings | undefined, additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions }, @@ -148,6 +150,7 @@ export const getCompletionsAtPosition = ( prior: prior!, fullText: sourceFile.getFullText(), typeChecker: program.getTypeChecker(), + languageServiceHost, } satisfies typeof sharedCompletionContext) if (node && !hasSuggestions && ensurePrior() && prior) { @@ -376,6 +379,7 @@ export const getCompletionsAtPosition = ( } if (!prior.isMemberCompletion) { + prior.entries = [...prior.entries, ...(filesAutoImport() ?? [])] prior.entries = markOrRemoveGlobalCompletions(prior.entries, position, languageService, c) ?? prior.entries } if (exactNode) { diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 64af965..0eebc46 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -87,7 +87,7 @@ export const decorateLanguageService = ( if (!scriptSnapshot) return const compilerOptions = languageServiceHost.getCompilationSettings() try { - const result = getCompletionsAtPosition(fileName, position, options, c, languageService, scriptSnapshot, formatOptions, { + const result = getCompletionsAtPosition(fileName, position, options, c, languageService, languageServiceHost, scriptSnapshot, formatOptions, { scriptKind, compilerOptions, }) diff --git a/typescript/src/ipcTypes.ts b/typescript/src/ipcTypes.ts index 2bd55d9..c45fc52 100644 --- a/typescript/src/ipcTypes.ts +++ b/typescript/src/ipcTypes.ts @@ -77,6 +77,8 @@ export type RequestInputTypes = { * @keysSuggestions TriggerCharacterCommand */ export type RequestOutputTypes = { + getNodePath: NodeAtPositionResponse[] + getNodeAtPosition: NodeAtPositionResponse removeFunctionArgumentsTypesInSelection: { ranges: TsRange[] } diff --git a/typescript/src/specialCommands/handle.ts b/typescript/src/specialCommands/handle.ts index 1e10a75..bd812bb 100644 --- a/typescript/src/specialCommands/handle.ts +++ b/typescript/src/specialCommands/handle.ts @@ -40,7 +40,15 @@ export default ( const node = findChildContainingPosition(ts, sourceFile, position) const posEnd = { pos: specialCommandArg.range[0], end: specialCommandArg.range[1] } - const extendedCodeActions = getExtendedCodeActions(sourceFile, posEnd, languageService, undefined, undefined, specialCommandArg.diagnostics) + const extendedCodeActions = getExtendedCodeActions( + sourceFile, + posEnd, + languageService, + undefined, + undefined, + configuration, + specialCommandArg.diagnostics, + ) return { turnArrayIntoObject: objectIntoArrayConverters(posEnd, node, undefined), extendedCodeActions, @@ -56,6 +64,7 @@ export default ( languageService, formatOptions, applyCodeActionTitle, + configuration, ) satisfies RequestOutputTypes['getExtendedCodeActionEdits'] } if (specialCommand === 'twoStepCodeActionSecondStep') { diff --git a/typescript/test/codeActions/addDestruct.spec.ts b/typescript/test/codeActions/addDestruct.spec.ts index 357a9ef..45433eb 100644 --- a/typescript/test/codeActions/addDestruct.spec.ts +++ b/typescript/test/codeActions/addDestruct.spec.ts @@ -26,6 +26,37 @@ describe('Add destructure', () => { const { codeAction } = fourslashLikeTester(initial, undefined, { dedent: true }) + codeAction(0, { + refactorName: 'Add Destruct', + newContent: expected, + }) + }) + test('Multiple same prop extractions', () => { + const initial = /* ts */ ` + const /*t*/props/*t*/ = { + source: { + type: Object, + required: true, + }, + }; + const test = props.source; + const test2 = props.source; + const test3 = props.source; + ` + const expected = /* ts */ ` + const { source } = { + source: { + type: Object, + required: true, + }, + }; + const test = source; + const test2 = source; + const test3 = source; + ` + + const { codeAction } = fourslashLikeTester(initial, undefined, { dedent: true }) + codeAction(0, { refactorName: 'Add Destruct', newContent: expected, diff --git a/typescript/test/codeActions/fromDestruct.spec.ts b/typescript/test/codeActions/fromDestruct.spec.ts index 89541d5..6ad32f8 100644 --- a/typescript/test/codeActions/fromDestruct.spec.ts +++ b/typescript/test/codeActions/fromDestruct.spec.ts @@ -52,10 +52,10 @@ describe('From destructure', () => { ) expect(content).toMatchInlineSnapshot(` - " - const something: number = anObject.something; - " - `) + " + const something: number = anObject.something; + " + `) }) }) test('Should convert nested', () => { @@ -204,12 +204,12 @@ describe('From destructure', () => { { compareContent: true }, ) expect(newContent).toMatchInlineSnapshot(` - " - const foo = { - foo: 1, - }.foo - " - `) + " + const foo = { + foo: 1, + }.foo + " + `) }) test('Destructured two or more properties', () => { const initial = /* ts*/ ` diff --git a/typescript/test/testing.ts b/typescript/test/testing.ts index d7ecce8..a8a5a4d 100644 --- a/typescript/test/testing.ts +++ b/typescript/test/testing.ts @@ -45,6 +45,7 @@ export const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, s }, defaultConfigFunc, languageService, + languageServiceHost, languageServiceHost.getScriptSnapshot(entrypoint)!, { convertTabsToSpaces: false,