Skip to content

Commit 687d946

Browse files
committed
feat: Add absolutely massive feature for searching / replacing or selecting semantically different parts of TS/JS code. Add goToNodeBySyntaxKind and goToNodeBySyntaxKindWithinBlock commands. Filtering is simple and happens by kind property of node.
1 parent f3fca6d commit 687d946

File tree

7 files changed

+169
-33
lines changed

7 files changed

+169
-33
lines changed

README.MD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Top Features
44

5+
### Special Commands
6+
7+
See [special commands list](#special-commands-list)
8+
59
### JSX Outline
610

711
(*disabled by default*) Enable with `tsEssentialPlugins.patchOutline`
@@ -144,6 +148,12 @@ type A<T extends 'foo' | 'bar' = ''> = ...
144148
145149
### Builtin CodeFix Fixes
146150
151+
## Special Commands List
152+
153+
### Go to / Select Nodes by Kind
154+
155+
Use cases: search excluding comments, search & replace only within strings, find interested JSX attribute node
156+
147157
## Even Even More
148158
149159
Please look at extension settings, as this extension has much more features than described here!

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
"command": "pickAndInsertFunctionArguments",
4242
"title": "Pick and Insert Function Arguments",
4343
"category": "TS Essentials"
44+
},
45+
{
46+
"command": "goToNodeBySyntaxKind",
47+
"title": "Go to Node by Syntax Kind",
48+
"category": "TS Essentials"
49+
},
50+
{
51+
"command": "goToNodeBySyntaxKindWithinBlock",
52+
"title": "Go to Node by Syntax Kind Within Block",
53+
"category": "TS Essentials"
4454
}
4555
],
4656
"typescriptServerPlugins": [

src/specialCommands.ts

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as vscode from 'vscode'
2-
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
3-
import { registerExtensionCommand } from 'vscode-framework'
2+
import { getActiveRegularEditor, rangeToSelection } from '@zardoy/vscode-utils'
3+
import { getExtensionCommandId, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
44
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
5+
import _ from 'lodash'
6+
import { compact } from '@zardoy/utils'
57
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
68
import { sendCommand } from './sendCommand'
7-
import { tsRangeToVscode } from './util'
9+
import { tsRangeToVscode, tsRangeToVscodeSelection } from './util'
810

911
export default () => {
1012
registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => {
@@ -55,45 +57,51 @@ export default () => {
5557
editor.selection = new vscode.Selection(currentValueRange.start, currentValueRange.end)
5658
})
5759

58-
registerExtensionCommand('pickAndInsertFunctionArguments', async () => {
59-
const editor = getActiveRegularEditor()
60-
if (!editor) return
61-
const result = await sendCommand<RequestResponseTypes['pickAndInsertFunctionArguments']>('pickAndInsertFunctionArguments')
62-
if (!result) return
60+
const nodePicker = async <T>(data: T[], renderItem: (item: T) => Omit<VSCodeQuickPickItem, 'value'> & { nodeRange: [number, number] }) => {
61+
const editor = vscode.window.activeTextEditor!
6362
const originalSelections = editor.selections
64-
65-
const renderArgs = (args: Array<[name: string, type: string]>) => `${args.map(([name, type]) => (type ? `${name}: ${type}` : name)).join(', ')}`
66-
6763
let revealBack = true
68-
const selectedFunction = await showQuickPick(
69-
result.functions.map(func => {
70-
const [name, _decl, args] = func
64+
65+
// todo-p1 button to merge nodes with duplicated contents (e.g. same strings)
66+
const selected = await showQuickPick(
67+
data.map(item => {
68+
const custom = renderItem(item)
7169
return {
72-
label: name,
73-
value: func,
74-
description: `(${renderArgs(args)})`,
70+
...custom,
71+
value: item,
7572
buttons: [
7673
{
7774
iconPath: new vscode.ThemeIcon('go-to-file'),
7875
tooltip: 'Go to declaration',
76+
action: 'goToStartPos',
77+
},
78+
{
79+
iconPath: new vscode.ThemeIcon('arrow-both'),
80+
tooltip: 'Add to selection',
81+
action: 'addSelection',
7982
},
8083
],
8184
}
8285
}),
8386
{
84-
onDidTriggerItemButton(event) {
87+
title: 'Select node...',
88+
onDidTriggerItemButton({ item, button }) {
89+
const { action } = button as any
90+
const sel = tsRangeToVscodeSelection(editor.document, (item as any).nodeRange)
8591
revealBack = false
86-
this.hide()
87-
const pos = editor.document.positionAt(event.item.value[1][0])
88-
editor.selection = new vscode.Selection(pos, pos)
89-
editor.revealRange(editor.selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport)
92+
if (action === 'goToStartPos') {
93+
editor.selection = new vscode.Selection(sel.start, sel.start)
94+
editor.revealRange(editor.selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport)
95+
this.hide()
96+
} else {
97+
editor.selections = [...editor.selections, sel]
98+
}
9099
},
91100
onDidChangeFirstActive(item) {
92-
const pos = editor.document.positionAt(item.value[1][0])
101+
const pos = editor.document.positionAt((item as any).nodeRange[0])
93102
editor.selection = new vscode.Selection(pos, pos)
94103
editor.revealRange(editor.selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport)
95104
},
96-
onDidShow() {},
97105
matchOnDescription: true,
98106
},
99107
)
@@ -102,6 +110,23 @@ export default () => {
102110
editor.revealRange(editor.selection)
103111
}
104112

113+
return selected
114+
}
115+
116+
registerExtensionCommand('pickAndInsertFunctionArguments', async () => {
117+
const editor = getActiveRegularEditor()
118+
if (!editor) return
119+
const result = await sendCommand<RequestResponseTypes['pickAndInsertFunctionArguments']>('pickAndInsertFunctionArguments')
120+
if (!result) return
121+
122+
const renderArgs = (args: Array<[name: string, type: string]>) => `${args.map(([name, type]) => (type ? `${name}: ${type}` : name)).join(', ')}`
123+
124+
const selectedFunction = await nodePicker(result.functions, ([name, decl, args]) => ({
125+
label: name,
126+
description: `(${renderArgs(args)})`,
127+
nodeRange: decl,
128+
}))
129+
105130
if (!selectedFunction) return
106131
const selectedArgs = await showQuickPick(
107132
selectedFunction[2].map(arg => {
@@ -130,4 +155,64 @@ export default () => {
130155
}
131156
})
132157
})
158+
159+
registerExtensionCommand('goToNodeBySyntaxKind', async (_arg, { filterWithSelection = false }: { filterWithSelection?: boolean } = {}) => {
160+
const editor = vscode.window.activeTextEditor
161+
if (!editor) return
162+
const { document } = editor
163+
const result = await sendCommand<RequestResponseTypes['filterBySyntaxKind']>('filterBySyntaxKind')
164+
if (!result) return
165+
// todo optimize
166+
if (filterWithSelection) {
167+
result.nodesByKind = Object.fromEntries(
168+
compact(
169+
Object.entries(result.nodesByKind).map(([kind, nodes]) => {
170+
const filteredNodes = nodes.filter(({ range: tsRange }) =>
171+
editor.selections.some(sel => sel.contains(tsRangeToVscode(document, tsRange))),
172+
)
173+
if (filteredNodes.length === 0) return
174+
return [kind, filteredNodes]
175+
}),
176+
),
177+
)
178+
}
179+
180+
const selectedKindNodes = await showQuickPick(
181+
_.sortBy(Object.entries(result.nodesByKind), ([, nodes]) => nodes.length)
182+
.reverse()
183+
.map(([kind, nodes]) => ({
184+
label: kind,
185+
description: nodes.length.toString(),
186+
value: nodes,
187+
buttons: [
188+
{
189+
iconPath: new vscode.ThemeIcon('arrow-both'),
190+
tooltip: 'Select all nodes of this kind',
191+
},
192+
],
193+
})),
194+
{
195+
onDidTriggerItemButton(button) {
196+
editor.selections = button.item.value.map(({ range }) => tsRangeToVscodeSelection(document, range))
197+
this.hide()
198+
},
199+
},
200+
)
201+
if (!selectedKindNodes) return
202+
const selectedNode = await nodePicker(selectedKindNodes, node => ({
203+
label: document
204+
.getText(tsRangeToVscode(document, node.range))
205+
.trim()
206+
.replace(/\r?\n\s+/g, ' '),
207+
nodeRange: node.range,
208+
value: node,
209+
}))
210+
if (!selectedNode) return
211+
editor.selection = tsRangeToVscodeSelection(document, selectedNode.range)
212+
editor.revealRange(editor.selection)
213+
})
214+
215+
registerExtensionCommand('goToNodeBySyntaxKindWithinSelection', async () => {
216+
await vscode.commands.executeCommand(getExtensionCommandId('goToNodeBySyntaxKind'), { filterWithSelection: true })
217+
})
133218
}

src/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ import * as vscode from 'vscode'
22

33
export const tsRangeToVscode = (document: vscode.TextDocument, [start, end]: [number, number]) =>
44
new vscode.Range(document.positionAt(start), document.positionAt(end))
5+
6+
export const tsRangeToVscodeSelection = (document: vscode.TextDocument, [start, end]: [number, number]) =>
7+
new vscode.Selection(document.positionAt(start), document.positionAt(end))

typescript/src/definitions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ export default (proxy: ts.LanguageService, info: ts.server.PluginCreateInfo, c:
44
proxy.getDefinitionAndBoundSpan = (fileName, position) => {
55
const prior = info.languageService.getDefinitionAndBoundSpan(fileName, position)
66
if (!prior) return
7-
if (__WEB__)
7+
if (__WEB__) {
88
// let extension handle it
99
// TODO failedAliasResolution
1010
prior.definitions = prior.definitions?.filter(def => {
1111
return !def.unverified || def.fileName === fileName
1212
})
13+
}
1314

1415
// used after check
1516
const firstDef = prior.definitions![0]!

typescript/src/ipcTypes.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@ export type NodeAtPositionResponse = {
1818
end: number
1919
}
2020

21-
export type PickFunctionArgsType = [name: string, declaration: [number, number], args: [name: string, type: string][]]
21+
type TsRange = [number, number]
22+
23+
export type PickFunctionArgsType = [name: string, declaration: TsRange, args: [name: string, type: string][]]
2224

2325
export type RequestResponseTypes = {
2426
removeFunctionArgumentsTypesInSelection: {
25-
ranges: [number, number][]
27+
ranges: TsRange[]
2628
}
2729
getRangeOfSpecialValue: {
28-
range: [number, number]
30+
range: TsRange
2931
}
3032
pickAndInsertFunctionArguments: {
3133
functions: PickFunctionArgsType[]
3234
}
35+
filterBySyntaxKind: {
36+
nodesByKind: Record<string, Array<{ range: TsRange }>>
37+
}
3338
}
3439

3540
export type RequestOptionsTypes = {

typescript/src/specialCommands/handle.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { compact } from '@zardoy/utils'
22
import postfixesAtPosition from '../completions/postfixesAtPosition'
3-
import { PickFunctionArgsType, NodeAtPositionResponse, RequestOptionsTypes, RequestResponseTypes, TriggerCharacterCommand, triggerCharacterCommands } from '../ipcTypes'
3+
import { NodeAtPositionResponse, RequestOptionsTypes, RequestResponseTypes, TriggerCharacterCommand, triggerCharacterCommands } from '../ipcTypes'
44
import { findChildContainingPosition, getNodePath } from '../utils'
55
import getEmmetCompletions from './emmet'
66

@@ -93,14 +93,14 @@ export default (
9393
}
9494
}
9595

96-
if(!targetNode) {
96+
if (!targetNode) {
9797
ts.findAncestor(node, (n) => {
9898
if (ts.isVariableDeclaration(n) && n.initializer && position < n.initializer.pos) {
9999
targetNode = n.initializer
100100
return true
101101
}
102102
if (ts.isCallExpression(n) && position < n.expression.end) {
103-
const pos = n.expression.end+1
103+
const pos = n.expression.end + 1
104104
targetNode = [pos, pos]
105105
return true
106106
}
@@ -129,6 +129,8 @@ export default (
129129
range: Array.isArray(targetNode) ? targetNode : [targetNode.pos, targetNode.end],
130130
} satisfies RequestResponseTypes['getRangeOfSpecialValue'],
131131
}
132+
} else {
133+
return
132134
}
133135
}
134136
if (specialCommand === 'pickAndInsertFunctionArguments') {
@@ -146,7 +148,7 @@ export default (
146148
entries: [],
147149
typescriptEssentialsResponse: {
148150
functions: collectedNodes.map((arr) => {
149-
return [arr[0], [arr[1].pos, arr[1].end], compact(arr[2].map(({name, type}) => {
151+
return [arr[0], [arr[1].pos, arr[1].end], compact(arr[2].map(({ name, type }) => {
150152
// or maybe allow?
151153
if (!ts.isIdentifier(name)) return
152154
return [name.text, type?.getText() ?? ''];
@@ -155,9 +157,29 @@ export default (
155157
} satisfies RequestResponseTypes['pickAndInsertFunctionArguments'],
156158
}
157159
}
160+
if (specialCommand === 'filterBySyntaxKind') {
161+
const collectedNodes: RequestResponseTypes['filterBySyntaxKind']['nodesByKind'] = {}
162+
collectedNodes.comment ??= []
163+
const collectNodes = (node: ts.Node) => {
164+
const kind = ts.SyntaxKind[node.kind]!;
165+
const leadingTrivia = node.getLeadingTriviaWidth(sourceFile)
166+
const comments = [...tsFull.getLeadingCommentRangesOfNode(node as any, sourceFile as any) ?? [], ...tsFull.getTrailingCommentRanges(node as any, sourceFile as any) ?? []]
167+
collectedNodes.comment!.push(...comments?.map((comment) => ({ range: [comment.pos, comment.end] as [number, number] })))
168+
collectedNodes[kind] ??= []
169+
collectedNodes[kind]!.push({ range: [node.pos + leadingTrivia, node.end] })
170+
node.forEachChild(collectNodes)
171+
}
172+
sourceFile.forEachChild(collectNodes)
173+
return {
174+
entries: [],
175+
typescriptEssentialsResponse: {
176+
nodesByKind: collectedNodes
177+
} satisfies RequestResponseTypes['filterBySyntaxKind']
178+
}
179+
}
158180
}
159181

160-
function changeType<T>(arg): asserts arg is T {}
182+
function changeType<T>(arg): asserts arg is T { }
161183

162184
function nodeToApiResponse(node: ts.Node): NodeAtPositionResponse {
163185
return {

0 commit comments

Comments
 (0)