Skip to content

Commit a4ff083

Browse files
committed
feat: Add a new extremely powerful way to search / replace occurrences of Search text only **within specific nodes by kind** (e.g. only in string / JSX Text) Search Workspace by Syntax Kind
1 parent 539e3a0 commit a4ff083

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878
{
7979
"command": "wrapIntoNewTag",
8080
"title": "Wrap Into New Tag"
81+
},
82+
{
83+
"command": "searchWorkspaceBySyntaxKind",
84+
"title": "Search Workspace by Syntax Kind"
8185
}
8286
],
8387
"keybindings": [

src/specialCommands.ts

+88
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,94 @@ export default () => {
352352
return
353353
})
354354

355+
registerExtensionCommand('searchWorkspaceBySyntaxKind', async () => {
356+
const result = await sendCommand('searchWorkspaceBySyntaxKindPrepare', {})
357+
if (!result) return
358+
const { syntaxKinds, filesCount } = result
359+
const selected = await showQuickPick(
360+
syntaxKinds.map(syntaxKind => ({ label: syntaxKind, value: syntaxKind })),
361+
{
362+
title: `Select syntax kind for filtering in ${filesCount} files`,
363+
canPickMany: true,
364+
ignoreFocusOut: true,
365+
},
366+
)
367+
if (!selected) return
368+
const searchQuery = await vscode.window.showInputBox({
369+
prompt: 'Enter search query',
370+
})
371+
if (!searchQuery) return
372+
void vscode.window.showInformationMessage('Processing search...')
373+
const result2 = await sendCommand('searchWorkspaceBySyntaxKind', {
374+
inputOptions: {
375+
query: searchQuery,
376+
kinds: selected,
377+
},
378+
})
379+
if (!result2) return
380+
const { files } = result2
381+
const results = [] as Array<{ document: vscode.TextDocument; range: vscode.Range }>
382+
for (const file of files) {
383+
const document = await vscode.workspace.openTextDocument(file.filename)
384+
// if (!document) continue
385+
for (const range of file.ranges) {
386+
results.push({ document, range: tsRangeToVscode(document, range) })
387+
}
388+
}
389+
390+
let replaceMode = false
391+
const displayFilesPicker = async () => {
392+
const selectedRange = await showQuickPick(
393+
results.map(file => ({
394+
label: file.document.fileName,
395+
value: file,
396+
})),
397+
{
398+
title: `Found ${results.length} results`,
399+
canPickMany: replaceMode,
400+
ignoreFocusOut: true,
401+
buttons: [
402+
{
403+
iconPath: new vscode.ThemeIcon('replace-all'),
404+
tooltip: 'Toggle replace mode enabled',
405+
},
406+
],
407+
onDidTriggerButton(event) {
408+
replaceMode = !replaceMode
409+
this.hide()
410+
void displayFilesPicker()
411+
},
412+
},
413+
)
414+
if (!selectedRange) return
415+
if (Array.isArray(selectedRange)) {
416+
const replaceFor = await vscode.window.showInputBox({
417+
prompt: 'Enter replace for',
418+
ignoreFocusOut: true,
419+
})
420+
if (!replaceFor) return
421+
422+
const rangesByFile = _.groupBy(selectedRange, file => file.document.fileName)
423+
for (const [_, ranges] of Object.entries(rangesByFile)) {
424+
const { document } = ranges[0]!
425+
const editor = await vscode.window.showTextDocument(document)
426+
// todo
427+
// eslint-disable-next-line no-await-in-loop
428+
await editor.edit(editBuilder => {
429+
for (const file of ranges) {
430+
editBuilder.replace(file.range, replaceFor)
431+
}
432+
})
433+
}
434+
} else {
435+
const { document, range } = selectedRange as any
436+
await vscode.window.showTextDocument(document, { selection: range })
437+
}
438+
}
439+
440+
await displayFilesPicker()
441+
})
442+
355443
// registerExtensionCommand('insertImportFlatten', () => {
356444
// // got -> default, got
357445
// type A = ts.Type

typescript/src/ipcTypes.ts

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const triggerCharacterCommands = [
1818
'getArgumentReferencesFromCurrentParameter',
1919
'performanceInfo',
2020
'getMigrateToImportsEdits',
21+
'searchWorkspaceBySyntaxKind',
22+
'searchWorkspaceBySyntaxKindPrepare',
2123
] as const
2224

2325
export type TriggerCharacterCommand = (typeof triggerCharacterCommands)[number]
@@ -70,6 +72,11 @@ export type RequestInputTypes = {
7072
range: [number, number]
7173
applyCodeActionTitle: string
7274
}
75+
76+
searchWorkspaceBySyntaxKind: {
77+
kinds: string[]
78+
query: string
79+
}
7380
}
7481

7582
// OUTPUT
@@ -120,6 +127,16 @@ export type RequestOutputTypes = {
120127
getArgumentReferencesFromCurrentParameter: Array<{ line: number; character: number; filename: string }>
121128
'emmet-completions': EmmetResult
122129
getMigrateToImportsEdits: ts.TextChange[]
130+
searchWorkspaceBySyntaxKindPrepare: {
131+
filesCount: number
132+
syntaxKinds: string[]
133+
}
134+
searchWorkspaceBySyntaxKind: {
135+
files: Array<{
136+
filename: string
137+
ranges: TsRange[]
138+
}>
139+
}
123140
}
124141

125142
// export type EmmetResult = {

typescript/src/specialCommands/handle.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { compact } from '@zardoy/utils'
22
import { getExtendedCodeActions } from '../codeActions/getCodeActions'
33
import { NodeAtPositionResponse, RequestInputTypes, RequestOutputTypes, TriggerCharacterCommand, triggerCharacterCommands } from '../ipcTypes'
44
import { GetConfig } from '../types'
5-
import { findChildContainingExactPosition, findChildContainingPosition, getNodePath } from '../utils'
5+
import { findChildContainingExactPosition, findChildContainingPosition, findClosestParent, getNodePath } from '../utils'
66
import { lastResolvedCompletion } from '../completionEntryDetails'
77
import { overrideRenameRequest } from '../decorateFindRenameLocations'
88
import getEmmetCompletions from './emmet'
@@ -254,6 +254,63 @@ export default (
254254
if (specialCommand === 'getLastResolvedCompletion') {
255255
return lastResolvedCompletion.value
256256
}
257+
if (specialCommand === 'searchWorkspaceBySyntaxKind' || specialCommand === 'searchWorkspaceBySyntaxKindPrepare') {
258+
const files = languageService
259+
.getProgram()!
260+
.getSourceFiles()
261+
.filter(x => !x.fileName.includes('node_modules') && !x.fileName.includes('dist') && !x.fileName.includes('build'))
262+
const excludeKinds: Array<keyof typeof ts.SyntaxKind> = ['SourceFile']
263+
const allowKinds: Array<keyof typeof ts.SyntaxKind> = ['ReturnStatement']
264+
if (specialCommand === 'searchWorkspaceBySyntaxKind') {
265+
changeType<RequestInputTypes['searchWorkspaceBySyntaxKind']>(specialCommandArg)
266+
267+
const collectedNodes: RequestOutputTypes['searchWorkspaceBySyntaxKind']['files'] = []
268+
for (const file of files) {
269+
let lastIndex = 0
270+
while (lastIndex !== -1) {
271+
lastIndex = file.text.indexOf(specialCommandArg.query, lastIndex + 1)
272+
if (lastIndex === -1) continue
273+
const node = findChildContainingExactPosition(file, lastIndex)
274+
if (!node || !specialCommandArg.kinds.includes(ts.SyntaxKind[node.kind]!)) continue
275+
276+
// ignore imports for now...
277+
const importDecl = findClosestParent(node, [ts.SyntaxKind.ImportDeclaration, ts.SyntaxKind.ExportDeclaration], [])
278+
if (importDecl) continue
279+
280+
const fileRanges = collectedNodes.find(x => x.filename === file.fileName)
281+
let start = node.pos + (specialCommandArg.kinds.includes('comment') ? 0 : node.getLeadingTriviaWidth(file))
282+
let endPos = node.end
283+
start += lastIndex - start
284+
endPos -= node.end - (lastIndex + specialCommandArg.query.length)
285+
const range = [start, endPos] as [number, number]
286+
if (fileRanges) {
287+
fileRanges.ranges.push(range)
288+
} else {
289+
collectedNodes.push({ filename: file.fileName, ranges: [range] })
290+
}
291+
}
292+
}
293+
294+
return {
295+
files: collectedNodes,
296+
} satisfies RequestOutputTypes['searchWorkspaceBySyntaxKind']
297+
}
298+
if (specialCommand === 'searchWorkspaceBySyntaxKindPrepare') {
299+
const kinds = Object.values(ts.SyntaxKind) as Array<string | number>
300+
return {
301+
syntaxKinds: kinds.filter(
302+
kind =>
303+
allowKinds.includes(kind as any) ||
304+
(typeof kind === 'string' &&
305+
!excludeKinds.includes(kind as any) &&
306+
!kind.includes('Token') &&
307+
!kind.includes('Statement') &&
308+
!kind.includes('Operator')),
309+
) as string[],
310+
filesCount: files.length,
311+
} satisfies RequestOutputTypes['searchWorkspaceBySyntaxKindPrepare']
312+
}
313+
}
257314
if (specialCommand === 'getFullType') {
258315
const text = getFullType(languageService, sourceFile, position)
259316
if (!text) return

0 commit comments

Comments
 (0)