Skip to content

Commit 0dcf3f7

Browse files
committed
feat: New code action! *Swap Keys and Values in Object*
1 parent ab8b877 commit 0dcf3f7

File tree

6 files changed

+110
-16
lines changed

6 files changed

+110
-16
lines changed

README.MD

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
## Top Features
44

5-
### Special Commands
5+
> Here React experience hits different!
66
7-
See [special commands list](#special-commands-list)
7+
### Special Commands & Actions
8+
9+
See [special commands list](#special-commands-list) ans [code actions list](#contributed-code-actions)
810

911
### JSX Outline
1012

@@ -154,6 +156,26 @@ type A<T extends 'foo' | 'bar' = ''> = ...
154156
155157
Use cases: search excluding comments, search & replace only within strings, find interested JSX attribute node
156158
159+
## Contributed Code Actions
160+
161+
### Swap Keys and Values in Object
162+
163+
> *Note*: Code action displayed **only** when object is fully explicitly selected
164+
165+
Example:
166+
167+
```ts
168+
const obj = {
169+
key1: 'someValue',
170+
key2: getSuperUniqueKey()
171+
}
172+
// turns into
173+
const obj = {
174+
'someValue': 'key1',
175+
[getSuperUniqueKey()]: 'key2'
176+
}
177+
```
178+
157179
## Even Even More
158180

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

typescript/src/codeActions/decorateProxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
99

1010
const program = languageService.getProgram()
1111
const sourceFile = program!.getSourceFile(fileName)!
12-
const { info: refactorInfo } = getCodeActions(ts, sourceFile, positionOrRange)
12+
const { info: refactorInfo } = getCodeActions(sourceFile, positionOrRange)
1313
if (refactorInfo) prior = [...prior, refactorInfo]
1414

1515
return prior
@@ -20,7 +20,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
2020
if (category === REFACTORS_CATEGORY) {
2121
const program = languageService.getProgram()
2222
const sourceFile = program!.getSourceFile(fileName)!
23-
const { edit } = getCodeActions(ts, sourceFile, positionOrRange, actionName)
23+
const { edit } = getCodeActions(sourceFile, positionOrRange, actionName)
2424
return edit
2525
}
2626
return languageService.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactorName, actionName, preferences)

typescript/src/codeActions/getCodeActions.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { compact } from '@zardoy/utils'
2-
import type tslib from 'typescript/lib/tsserverlibrary'
2+
import { findChildContainingPosition } from '../utils'
3+
import objectSwapKeysAndValues from './objectSwapKeysAndValues'
34
import toggleBraces from './toggleBraces'
45

56
type SimplifiedRefactorInfo = {
@@ -9,10 +10,10 @@ type SimplifiedRefactorInfo = {
910
}
1011

1112
export type ApplyCodeAction = (
12-
ts: typeof tslib,
1313
sourceFile: ts.SourceFile,
1414
position: number,
15-
range?: ts.TextRange,
15+
range: ts.TextRange | undefined,
16+
node: ts.Node | undefined,
1617
) => ts.RefactorEditInfo | SimplifiedRefactorInfo[] | undefined
1718

1819
export type CodeAction = {
@@ -21,22 +22,21 @@ export type CodeAction = {
2122
tryToApply: ApplyCodeAction
2223
}
2324

24-
const codeActions: CodeAction[] = [
25-
/* toggleBraces */
26-
]
25+
const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues]
2726

2827
export const REFACTORS_CATEGORY = 'essential-refactors'
2928

3029
export default (
31-
ts: typeof tslib,
3230
sourceFile: ts.SourceFile,
3331
positionOrRange: ts.TextRange | number,
3432
requestingEditsId?: string,
3533
): { info?: ts.ApplicableRefactorInfo; edit: ts.RefactorEditInfo } => {
3634
const range = typeof positionOrRange !== 'number' && positionOrRange.pos !== positionOrRange.end ? positionOrRange : undefined
35+
const pos = typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos
36+
const node = findChildContainingPosition(ts, sourceFile, pos)
3737
const appliableCodeActions = compact(
3838
codeActions.map(action => {
39-
const edits = action.tryToApply(ts, sourceFile, typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos, range)
39+
const edits = action.tryToApply(sourceFile, pos, range, node)
4040
if (!edits) return
4141
return {
4242
...action,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { approveCast } from '../utils'
2+
import { CodeAction } from './getCodeActions'
3+
4+
const nodeToSpan = (node: ts.Node): ts.TextSpan => {
5+
const start = node.pos + (node.getLeadingTriviaWidth() ?? 0)
6+
return { start, length: node.end - start }
7+
}
8+
9+
export default {
10+
id: 'objectSwapKeysAndValues',
11+
name: 'Swap Keys and Values in Object',
12+
tryToApply(sourceFile, _position, range, node) {
13+
if (!range || !node) return
14+
// requires full explicit object selection (be aware of comma) to not be annoying with suggestion
15+
if (!approveCast(node, ts.isObjectLiteralExpression) || !(range.pos === node.pos + node.getLeadingTriviaWidth() && range.end === node.end)) return
16+
const edits: ts.TextChange[] = []
17+
for (const property of node.properties) {
18+
if (!ts.isPropertyAssignment(property)) continue
19+
const { name, initializer } = property
20+
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+
}
31+
edits.push({
32+
newText: initializerText,
33+
span: nodeToSpan(name),
34+
})
35+
edits.push({
36+
newText: ts.isComputedPropertyName(name)
37+
? name.expression.getText()
38+
: ts.isIdentifier(name)
39+
? /* TODO quote preference */ `'${name.text}'`
40+
: name.getText(),
41+
span: nodeToSpan(initializer),
42+
})
43+
}
44+
if (!edits.length) return undefined
45+
return {
46+
edits: [
47+
{
48+
fileName: sourceFile.fileName,
49+
textChanges: edits,
50+
},
51+
],
52+
}
53+
},
54+
} satisfies CodeAction
55+
56+
// TODO!
57+
if (import.meta.vitest) {
58+
const { it, expect } = import.meta.vitest
59+
it('objectSwapKeysAndValues', () => {
60+
const case1 = /* ts */ `
61+
const a = /*1*/{
62+
// description
63+
test: /*inline comment?*/ 3,
64+
/** */['test2']: getComputedStyle.apply(),
65+
'test2': 'someValue'
66+
}/*2*/
67+
`
68+
})
69+
}

typescript/src/globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="vitest/importMeta" />
12
import('ts-expose-internals')
23
// prvided by esbuild at top-level of bundle in buildTsPlugin.mjs
34
declare let ts: typeof import('typescript')

typescript/src/utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,10 @@ const wordRangeAtPos = (text: string, position: number) => {
170170
return text.slice(startPos + 1, endPos)
171171
}
172172

173-
export function approveCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): value is TOut
174-
export function approveCast<T>(value: T, test: (value: T) => boolean): T | undefined
175-
export function approveCast<T>(value: T, test: (value: T) => boolean): T | undefined {
176-
return value !== undefined && test(value) ? value : undefined
173+
type GetIs<T> = T extends (elem: any) => elem is infer T ? T : never
174+
175+
export function approveCast<T2 extends Array<(node: ts.Node) => node is ts.Node>>(node: ts.Node | undefined, ...oneOfTest: T2): node is GetIs<T2[number]> {
176+
if (node === undefined) return false
177+
if (!oneOfTest) throw new Error('Tests are not provided')
178+
return oneOfTest.some(test => test(node))
177179
}

0 commit comments

Comments
 (0)