diff --git a/package.json b/package.json index 26a9f14..07d1c58 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,10 @@ { "command": "wrapIntoNewTag", "title": "Wrap Into New Tag" + }, + { + "command": "searchWorkspaceBySyntaxKind", + "title": "Search Workspace by Syntax Kind" } ], "keybindings": [ diff --git a/src/specialCommands.ts b/src/specialCommands.ts index 0e8a55a..f6194c1 100644 --- a/src/specialCommands.ts +++ b/src/specialCommands.ts @@ -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 diff --git a/typescript/src/ipcTypes.ts b/typescript/src/ipcTypes.ts index c45fc52..05cdd9b 100644 --- a/typescript/src/ipcTypes.ts +++ b/typescript/src/ipcTypes.ts @@ -18,6 +18,8 @@ export const triggerCharacterCommands = [ 'getArgumentReferencesFromCurrentParameter', 'performanceInfo', 'getMigrateToImportsEdits', + 'searchWorkspaceBySyntaxKind', + 'searchWorkspaceBySyntaxKindPrepare', ] as const export type TriggerCharacterCommand = (typeof triggerCharacterCommands)[number] @@ -70,6 +72,11 @@ export type RequestInputTypes = { range: [number, number] applyCodeActionTitle: string } + + searchWorkspaceBySyntaxKind: { + kinds: string[] + query: string + } } // OUTPUT @@ -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 = { diff --git a/typescript/src/specialCommands/handle.ts b/typescript/src/specialCommands/handle.ts index bd812bb..204cf0e 100644 --- a/typescript/src/specialCommands/handle.ts +++ b/typescript/src/specialCommands/handle.ts @@ -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' @@ -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 = ['SourceFile'] + const allowKinds: Array = ['ReturnStatement'] + if (specialCommand === 'searchWorkspaceBySyntaxKind') { + changeType(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 + 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