Skip to content

Commit

Permalink
feat: Add a new extremely powerful way to search / replace occurrence…
Browse files Browse the repository at this point in the history
…s of Search text only **within specific nodes by kind** (e.g. only in string / JSX Text) `Search Workspace by Syntax Kind`
  • Loading branch information
zardoy committed Feb 10, 2024
1 parent 539e3a0 commit a4ff083
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 1 deletion.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
{
"command": "wrapIntoNewTag",
"title": "Wrap Into New Tag"
},
{
"command": "searchWorkspaceBySyntaxKind",
"title": "Search Workspace by Syntax Kind"
}
],
"keybindings": [
Expand Down
88 changes: 88 additions & 0 deletions src/specialCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,94 @@ export default () => {
return
})

registerExtensionCommand('searchWorkspaceBySyntaxKind', async () => {
const result = await sendCommand('searchWorkspaceBySyntaxKindPrepare', {})
if (!result) return
const { syntaxKinds, filesCount } = result
const selected = await showQuickPick(
syntaxKinds.map(syntaxKind => ({ label: syntaxKind, value: syntaxKind })),
{
title: `Select syntax kind for filtering in ${filesCount} files`,
canPickMany: true,
ignoreFocusOut: true,
},
)
if (!selected) return
const searchQuery = await vscode.window.showInputBox({
prompt: 'Enter search query',
})
if (!searchQuery) return
void vscode.window.showInformationMessage('Processing search...')
const result2 = await sendCommand('searchWorkspaceBySyntaxKind', {
inputOptions: {
query: searchQuery,
kinds: selected,
},
})
if (!result2) return
const { files } = result2
const results = [] as Array<{ document: vscode.TextDocument; range: vscode.Range }>
for (const file of files) {
const document = await vscode.workspace.openTextDocument(file.filename)
// if (!document) continue
for (const range of file.ranges) {
results.push({ document, range: tsRangeToVscode(document, range) })
}
}

let replaceMode = false
const displayFilesPicker = async () => {
const selectedRange = await showQuickPick(
results.map(file => ({
label: file.document.fileName,
value: file,
})),
{
title: `Found ${results.length} results`,
canPickMany: replaceMode,
ignoreFocusOut: true,
buttons: [
{
iconPath: new vscode.ThemeIcon('replace-all'),
tooltip: 'Toggle replace mode enabled',
},
],
onDidTriggerButton(event) {
replaceMode = !replaceMode
this.hide()
void displayFilesPicker()
},
},
)
if (!selectedRange) return
if (Array.isArray(selectedRange)) {
const replaceFor = await vscode.window.showInputBox({
prompt: 'Enter replace for',
ignoreFocusOut: true,
})
if (!replaceFor) return

const rangesByFile = _.groupBy(selectedRange, file => file.document.fileName)
for (const [_, ranges] of Object.entries(rangesByFile)) {
const { document } = ranges[0]!
const editor = await vscode.window.showTextDocument(document)
// todo
// eslint-disable-next-line no-await-in-loop
await editor.edit(editBuilder => {
for (const file of ranges) {
editBuilder.replace(file.range, replaceFor)
}
})
}
} else {
const { document, range } = selectedRange as any
await vscode.window.showTextDocument(document, { selection: range })
}
}

await displayFilesPicker()
})

// registerExtensionCommand('insertImportFlatten', () => {
// // got -> default, got
// type A = ts.Type
Expand Down
17 changes: 17 additions & 0 deletions typescript/src/ipcTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const triggerCharacterCommands = [
'getArgumentReferencesFromCurrentParameter',
'performanceInfo',
'getMigrateToImportsEdits',
'searchWorkspaceBySyntaxKind',
'searchWorkspaceBySyntaxKindPrepare',
] as const

export type TriggerCharacterCommand = (typeof triggerCharacterCommands)[number]
Expand Down Expand Up @@ -70,6 +72,11 @@ export type RequestInputTypes = {
range: [number, number]
applyCodeActionTitle: string
}

searchWorkspaceBySyntaxKind: {
kinds: string[]
query: string
}
}

// OUTPUT
Expand Down Expand Up @@ -120,6 +127,16 @@ export type RequestOutputTypes = {
getArgumentReferencesFromCurrentParameter: Array<{ line: number; character: number; filename: string }>
'emmet-completions': EmmetResult
getMigrateToImportsEdits: ts.TextChange[]
searchWorkspaceBySyntaxKindPrepare: {
filesCount: number
syntaxKinds: string[]
}
searchWorkspaceBySyntaxKind: {
files: Array<{
filename: string
ranges: TsRange[]
}>
}
}

// export type EmmetResult = {
Expand Down
59 changes: 58 additions & 1 deletion typescript/src/specialCommands/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { compact } from '@zardoy/utils'
import { getExtendedCodeActions } from '../codeActions/getCodeActions'
import { NodeAtPositionResponse, RequestInputTypes, RequestOutputTypes, TriggerCharacterCommand, triggerCharacterCommands } from '../ipcTypes'
import { GetConfig } from '../types'
import { findChildContainingExactPosition, findChildContainingPosition, getNodePath } from '../utils'
import { findChildContainingExactPosition, findChildContainingPosition, findClosestParent, getNodePath } from '../utils'
import { lastResolvedCompletion } from '../completionEntryDetails'
import { overrideRenameRequest } from '../decorateFindRenameLocations'
import getEmmetCompletions from './emmet'
Expand Down Expand Up @@ -254,6 +254,63 @@ export default (
if (specialCommand === 'getLastResolvedCompletion') {
return lastResolvedCompletion.value
}
if (specialCommand === 'searchWorkspaceBySyntaxKind' || specialCommand === 'searchWorkspaceBySyntaxKindPrepare') {
const files = languageService
.getProgram()!
.getSourceFiles()
.filter(x => !x.fileName.includes('node_modules') && !x.fileName.includes('dist') && !x.fileName.includes('build'))
const excludeKinds: Array<keyof typeof ts.SyntaxKind> = ['SourceFile']
const allowKinds: Array<keyof typeof ts.SyntaxKind> = ['ReturnStatement']
if (specialCommand === 'searchWorkspaceBySyntaxKind') {
changeType<RequestInputTypes['searchWorkspaceBySyntaxKind']>(specialCommandArg)

const collectedNodes: RequestOutputTypes['searchWorkspaceBySyntaxKind']['files'] = []
for (const file of files) {
let lastIndex = 0
while (lastIndex !== -1) {
lastIndex = file.text.indexOf(specialCommandArg.query, lastIndex + 1)
if (lastIndex === -1) continue
const node = findChildContainingExactPosition(file, lastIndex)
if (!node || !specialCommandArg.kinds.includes(ts.SyntaxKind[node.kind]!)) continue

// ignore imports for now...
const importDecl = findClosestParent(node, [ts.SyntaxKind.ImportDeclaration, ts.SyntaxKind.ExportDeclaration], [])
if (importDecl) continue

const fileRanges = collectedNodes.find(x => x.filename === file.fileName)
let start = node.pos + (specialCommandArg.kinds.includes('comment') ? 0 : node.getLeadingTriviaWidth(file))
let endPos = node.end
start += lastIndex - start
endPos -= node.end - (lastIndex + specialCommandArg.query.length)
const range = [start, endPos] as [number, number]
if (fileRanges) {
fileRanges.ranges.push(range)
} else {
collectedNodes.push({ filename: file.fileName, ranges: [range] })
}
}
}

return {
files: collectedNodes,
} satisfies RequestOutputTypes['searchWorkspaceBySyntaxKind']
}
if (specialCommand === 'searchWorkspaceBySyntaxKindPrepare') {
const kinds = Object.values(ts.SyntaxKind) as Array<string | number>
return {
syntaxKinds: kinds.filter(
kind =>
allowKinds.includes(kind as any) ||
(typeof kind === 'string' &&
!excludeKinds.includes(kind as any) &&
!kind.includes('Token') &&
!kind.includes('Statement') &&
!kind.includes('Operator')),
) as string[],
filesCount: files.length,
} satisfies RequestOutputTypes['searchWorkspaceBySyntaxKindPrepare']
}
}
if (specialCommand === 'getFullType') {
const text = getFullType(languageService, sourceFile, position)
if (!text) return
Expand Down

0 comments on commit a4ff083

Please sign in to comment.