Skip to content

Commit d61f9be

Browse files
committed
feat: New code action (another refactoring): *Turn Array Into Object*. Its huge!
1 parent 0dcf3f7 commit d61f9be

File tree

8 files changed

+267
-47
lines changed

8 files changed

+267
-47
lines changed

README.MD

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,39 @@ const obj = {
176176
}
177177
```
178178

179+
### Turn Array Into Object
180+
181+
```ts
182+
const data = [
183+
{
184+
// test
185+
key: 'bar',
186+
a: 0
187+
},
188+
{
189+
key: 'baz',
190+
// yes
191+
b: 1
192+
}
193+
]
194+
```
195+
196+
After selecting code action, you'll get asked for key to used (here you can use only `key`) and after applying:
197+
198+
```ts
199+
const a = {
200+
'bar': {
201+
a: 0
202+
},
203+
'baz': {
204+
// yes
205+
b: 1
206+
}
207+
}
208+
```
209+
210+
(note that for now refactoring removes properties with comments!)
211+
179212
## Even Even More
180213

181214
Please look at extension settings, as this extension has much more features than described here!

src/sendCommand.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { getActiveRegularEditor } from '@zardoy/vscode-utils'
33
import { getExtensionSetting } from 'vscode-framework'
44
import { TriggerCharacterCommand } from '../typescript/src/ipcTypes'
55

6-
type SendCommandData = {
6+
type SendCommandData<K> = {
77
position: vscode.Position
88
document: vscode.TextDocument
9-
inputOptions?: any
9+
inputOptions?: K
1010
}
11-
export const sendCommand = async <T>(command: TriggerCharacterCommand, sendCommandDataArg?: SendCommandData): Promise<T | undefined> => {
11+
export const sendCommand = async <T, K = any>(command: TriggerCharacterCommand, sendCommandDataArg?: SendCommandData<K>): Promise<T | undefined> => {
1212
// plugin id disabled, languageService would not understand the special trigger character
1313
if (!getExtensionSetting('enablePlugin')) return
1414

@@ -19,7 +19,7 @@ export const sendCommand = async <T>(command: TriggerCharacterCommand, sendComma
1919
const {
2020
document: { uri },
2121
position,
22-
} = ((): SendCommandData => {
22+
} = ((): SendCommandData<any> => {
2323
if (sendCommandDataArg) return sendCommandDataArg
2424
const editor = getActiveRegularEditor()!
2525
return {

src/specialCommands.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as vscode from 'vscode'
2-
import { getActiveRegularEditor, rangeToSelection } from '@zardoy/vscode-utils'
2+
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
33
import { getExtensionCommandId, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
44
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
55
import _ from 'lodash'
66
import { compact } from '@zardoy/utils'
7+
import { defaultJsSupersetLangsWithVue } from '@zardoy/vscode-utils/build/langs'
8+
import { offsetPosition } from '@zardoy/vscode-utils/build/position'
79
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
810
import { sendCommand } from './sendCommand'
911
import { tsRangeToVscode, tsRangeToVscodeSelection } from './util'
@@ -13,12 +15,15 @@ export default () => {
1315
const editor = getActiveRegularEditor()
1416
if (!editor) return
1517
const { selection, document } = editor
16-
const response = await sendCommand<RequestResponseTypes['removeFunctionArgumentsTypesInSelection']>('removeFunctionArgumentsTypesInSelection', {
18+
const response = await sendCommand<
19+
RequestResponseTypes['removeFunctionArgumentsTypesInSelection'],
20+
RequestOptionsTypes['removeFunctionArgumentsTypesInSelection']
21+
>('removeFunctionArgumentsTypesInSelection', {
1722
document,
1823
position: selection.start,
1924
inputOptions: {
2025
endSelection: document.offsetAt(selection.end),
21-
} as RequestOptionsTypes['removeFunctionArgumentsTypesInSelection'],
26+
},
2227
})
2328
if (!response) return
2429
const { ranges } = response
@@ -215,4 +220,74 @@ export default () => {
215220
registerExtensionCommand('goToNodeBySyntaxKindWithinSelection', async () => {
216221
await vscode.commands.executeCommand(getExtensionCommandId('goToNodeBySyntaxKind'), { filterWithSelection: true })
217222
})
223+
224+
async function sendTurnIntoArrayRequest<T = RequestResponseTypes['turnArrayIntoObject']>(
225+
range: vscode.Range,
226+
selectedKeyName?: string,
227+
document = vscode.window.activeTextEditor!.document,
228+
) {
229+
return sendCommand<T, RequestOptionsTypes['turnArrayIntoObject']>('turnArrayIntoObject', {
230+
document,
231+
position: range.start,
232+
inputOptions: {
233+
range: [document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number],
234+
selectedKeyName,
235+
},
236+
})
237+
}
238+
239+
registerExtensionCommand('turnArrayIntoObjectRefactoring' as any, async (_, arg?: RequestResponseTypes['turnArrayIntoObject']) => {
240+
if (!arg) return
241+
const { keysCount, totalCount, totalObjectCount } = arg
242+
const selectedKey: string | false | undefined =
243+
// eslint-disable-next-line @typescript-eslint/dot-notation
244+
arg['key'] ||
245+
(await showQuickPick(
246+
Object.entries(keysCount).map(([key, count]) => {
247+
const isAllowed = count === totalObjectCount
248+
return { label: `${isAllowed ? '$(check)' : '$(close)'}${key}`, value: isAllowed ? key : false, description: `${count} hits` }
249+
}),
250+
{
251+
title: `Selected available key from ${totalObjectCount} objects (${totalCount} elements)`,
252+
},
253+
))
254+
if (selectedKey === undefined || selectedKey === '') return
255+
if (selectedKey === false) {
256+
void vscode.window.showWarningMessage("Can't use selected key as its not used in every object")
257+
return
258+
}
259+
260+
const editor = vscode.window.activeTextEditor!
261+
const edits = await sendTurnIntoArrayRequest<RequestResponseTypes['turnArrayIntoObjectEdit']>(editor.selection, selectedKey)
262+
if (!edits) throw new Error('Unknown error. Try debug.')
263+
await editor.edit(builder => {
264+
for (const { span, newText } of edits) {
265+
const start = editor.document.positionAt(span.start)
266+
builder.replace(new vscode.Range(start, offsetPosition(editor.document, start, span.length)), newText)
267+
}
268+
})
269+
})
270+
271+
// its actually a code action, but will be removed from there soon
272+
vscode.languages.registerCodeActionsProvider(defaultJsSupersetLangsWithVue, {
273+
async provideCodeActions(document, range, context, token) {
274+
if (context.triggerKind !== vscode.CodeActionTriggerKind.Invoke || document !== vscode.window.activeTextEditor?.document) return
275+
const result = await sendTurnIntoArrayRequest(range)
276+
if (!result) return
277+
const { keysCount, totalCount, totalObjectCount } = result
278+
return [
279+
{
280+
title: `Turn Array Into Object (${totalCount} elements)`,
281+
command: getExtensionCommandId('turnArrayIntoObjectRefactoring' as any),
282+
arguments: [
283+
{
284+
keysCount,
285+
totalCount,
286+
totalObjectCount,
287+
} satisfies RequestResponseTypes['turnArrayIntoObject'],
288+
],
289+
},
290+
]
291+
},
292+
})
218293
}

typescript/src/codeActions/getCodeActions.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { findChildContainingPosition } from '../utils'
33
import objectSwapKeysAndValues from './objectSwapKeysAndValues'
44
import toggleBraces from './toggleBraces'
55

6-
type SimplifiedRefactorInfo = {
7-
start: number
8-
length: number
9-
newText: string
10-
}
6+
type SimplifiedRefactorInfo =
7+
| {
8+
start: number
9+
length: number
10+
newText: string
11+
}
12+
| ts.TextChange[]
1113

1214
export type ApplyCodeAction = (
1315
sourceFile: ts.SourceFile,
@@ -45,14 +47,18 @@ export default (
4547
edits: [
4648
{
4749
fileName: sourceFile.fileName,
48-
textChanges: edits.map(({ length, newText, start }) => {
49-
return {
50-
newText,
51-
span: {
52-
length,
53-
start,
54-
},
50+
textChanges: edits.map(change => {
51+
if ('start' in change) {
52+
const { newText, start, length } = change
53+
return {
54+
newText,
55+
span: {
56+
length,
57+
start,
58+
},
59+
}
5560
}
61+
return change
5662
}),
5763
},
5864
],

typescript/src/codeActions/objectSwapKeysAndValues.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ const nodeToSpan = (node: ts.Node): ts.TextSpan => {
66
return { start, length: node.end - start }
77
}
88

9+
export const printNodeForObjectKey = (node: ts.Node) => {
10+
const needsComputedBraces = approveCast(node, ts.isStringLiteral, ts.isNumericLiteral)
11+
? false
12+
: approveCast(node, ts.isIdentifier, ts.isCallExpression, ts.isPropertyAccessExpression)
13+
? true
14+
: undefined
15+
if (needsComputedBraces === undefined) return
16+
let nodeText = node.getText()
17+
if (needsComputedBraces) {
18+
nodeText = `[${nodeText}]`
19+
}
20+
return nodeText
21+
}
22+
923
export default {
1024
id: 'objectSwapKeysAndValues',
1125
name: 'Swap Keys and Values in Object',
@@ -18,16 +32,8 @@ export default {
1832
if (!ts.isPropertyAssignment(property)) continue
1933
const { name, initializer } = property
2034
if (!name || ts.isPrivateIdentifier(name)) continue
21-
const needsComputedBraces = approveCast(initializer, ts.isStringLiteral, ts.isNumericLiteral)
22-
? false
23-
: approveCast(initializer, ts.isIdentifier, ts.isCallExpression, ts.isPropertyAccessExpression)
24-
? true
25-
: undefined
26-
if (needsComputedBraces === undefined) continue
27-
let initializerText = initializer.getText()
28-
if (needsComputedBraces) {
29-
initializerText = `[${initializerText}]`
30-
}
35+
const initializerText = printNodeForObjectKey(initializer)
36+
if (!initializerText) continue
3137
edits.push({
3238
newText: initializerText,
3339
span: nodeToSpan(name),

typescript/src/ipcTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const triggerCharacterCommands = [
88
'removeFunctionArgumentsTypesInSelection',
99
'pickAndInsertFunctionArguments',
1010
'getRangeOfSpecialValue',
11+
'turnArrayIntoObject',
1112
] as const
1213

1314
export type TriggerCharacterCommand = typeof triggerCharacterCommands[number]
@@ -22,6 +23,9 @@ type TsRange = [number, number]
2223

2324
export type PickFunctionArgsType = [name: string, declaration: TsRange, args: [name: string, type: string][]]
2425

26+
/**
27+
* @keysSuggestions TriggerCharacterCommand
28+
*/
2529
export type RequestResponseTypes = {
2630
removeFunctionArgumentsTypesInSelection: {
2731
ranges: TsRange[]
@@ -35,12 +39,22 @@ export type RequestResponseTypes = {
3539
filterBySyntaxKind: {
3640
nodesByKind: Record<string, Array<{ range: TsRange }>>
3741
}
42+
turnArrayIntoObject: {
43+
keysCount: Record<string, number>
44+
totalCount: number
45+
totalObjectCount: number
46+
}
47+
turnArrayIntoObjectEdit: ts.TextChange[]
3848
}
3949

4050
export type RequestOptionsTypes = {
4151
removeFunctionArgumentsTypesInSelection: {
4252
endSelection: number
4353
}
54+
turnArrayIntoObject: {
55+
range: [number, number]
56+
selectedKeyName?: string
57+
}
4458
}
4559

4660
// export type EmmetResult = {

0 commit comments

Comments
 (0)