From d6cf169376cfc59ea68bb2b8d666e22ffe13e81c Mon Sep 17 00:00:00 2001 From: Josh Mu Date: Fri, 26 Apr 2024 14:27:24 +1000 Subject: [PATCH] feat: refactor round 2, with shared context --- src/lib/context.ts | 47 ++++ src/lib/editorActions.ts | 7 +- src/lib/periscope.ts | 222 ++++++------------ src/lib/ripgrep.ts | 40 ++++ src/utils/formatPathLabel.ts | 42 ++++ ...Type.ts => highlightLineDecorationType.ts} | 0 6 files changed, 210 insertions(+), 148 deletions(-) create mode 100644 src/lib/context.ts create mode 100644 src/lib/ripgrep.ts create mode 100644 src/utils/formatPathLabel.ts rename src/utils/{decorationType.ts => highlightLineDecorationType.ts} (100%) diff --git a/src/lib/context.ts b/src/lib/context.ts new file mode 100644 index 0000000..6184153 --- /dev/null +++ b/src/lib/context.ts @@ -0,0 +1,47 @@ +import * as vscode from 'vscode'; +import { AllQPItemVariants, DisposablesMap } from '../types'; +import { getConfig } from '../utils/getConfig'; +import { ChildProcessWithoutNullStreams } from 'child_process'; +import { highlightLineDecorationType } from '../utils/highlightLineDecorationType'; + +// simple context for each invoke of periscope search +let qp = vscode.window.createQuickPick(); // @see https://code.visualstudio.com/api/references/vscode-api#QuickPick +let workspaceFolders = vscode.workspace.workspaceFolders; +let query = ''; +let spawnRegistry: ChildProcessWithoutNullStreams[] = []; +let config = getConfig(); +let rgMenuActionsSelected: string[] = []; +let highlightDecoration = highlightLineDecorationType(); +let disposables: DisposablesMap = { + general: [], + rgMenuActions: [], + query: [], +}; + +export const context = { + resetContext, + qp, + workspaceFolders, + query, + spawnRegistry, + config, + rgMenuActionsSelected, + highlightDecoration, + disposables +}; + +// reset the context +function resetContext() { + context.qp = vscode.window.createQuickPick(); + context.workspaceFolders = vscode.workspace.workspaceFolders; + context.query = ''; + context.spawnRegistry = []; + context.config = getConfig(); + context.rgMenuActionsSelected = []; + context.highlightDecoration = highlightLineDecorationType(); + context.disposables = { + general: [], + rgMenuActions: [], + query: [], + }; +} \ No newline at end of file diff --git a/src/lib/editorActions.ts b/src/lib/editorActions.ts index 654637b..c30e454 100644 --- a/src/lib/editorActions.ts +++ b/src/lib/editorActions.ts @@ -2,8 +2,7 @@ import * as vscode from 'vscode'; import { previousActiveEditor, updatePreviousActiveEditor } from './editorContext'; import { activeQP } from './quickpickContext'; import { AllQPItemVariants, QPItemQuery } from '../types'; -import { getConfig } from '../utils/getConfig'; -import { highlightLineDecorationType } from '../utils/decorationType'; +import {context as cx} from './context'; export function closePreviewEditor() { if(previousActiveEditor) { @@ -47,7 +46,7 @@ export function openNativeVscodeSearch(query: string, qp: vscode.QuickPick { - let qp: vscode.QuickPick; - let workspaceFolders = vscode.workspace.workspaceFolders; - let query = ''; - let spawnRegistry: ChildProcessWithoutNullStreams[] = []; - let config = getConfig(); - let rgMenuActionsSelected: string[] = []; - let disposables: DisposablesMap = { - general: [], - rgMenuActions: [], - query: [], - }; + // fresh context + cx.resetContext(); function register() { - setActiveContext(true); + // fresh context + cx.resetContext(); log('start'); - config = getConfig(); - workspaceFolders = vscode.workspace.workspaceFolders; + + setActiveContext(true); updatePreviousActiveEditor(vscode.window.activeTextEditor); - // @see https://code.visualstudio.com/api/references/vscode-api#QuickPick - qp = vscode.window.createQuickPick(); - updateActiveQP(qp); + // todo: check whether we actually need activeQP? + updateActiveQP(cx.qp); // if ripgrep actions are available then open preliminary quickpick - const openRgMenuActions = config.alwaysShowRgMenuActions && config.rgMenuActions.length > 0; + const openRgMenuActions = cx.config.alwaysShowRgMenuActions && cx.config.rgMenuActions.length > 0; openRgMenuActions ? setupRgMenuActions() : setupQuickPickForQuery(); - disposables.general.push( - qp.onDidHide(onDidHide) + cx.disposables.general.push( + cx.qp.onDidHide(onDidHide) ); - qp.show(); + cx.qp.show(); } function disposeAll() { - disposables.general.forEach(d => d.dispose()); - disposables.rgMenuActions.forEach(d => d.dispose()); - disposables.query.forEach(d => d.dispose()); + cx.disposables.general.forEach(d => d.dispose()); + cx.disposables.rgMenuActions.forEach(d => d.dispose()); + cx.disposables.query.forEach(d => d.dispose()); } function reset() { checkKillProcess(); - disposables.rgMenuActions.forEach(d => d.dispose()); - disposables.query.forEach(d => d.dispose()); - qp.busy = false; - qp.value = ''; - query = ''; - rgMenuActionsSelected = []; + cx.disposables.rgMenuActions.forEach(d => d.dispose()); + cx.disposables.query.forEach(d => d.dispose()); + cx.qp.busy = false; + cx.qp.value = ''; + cx.query = ''; + cx.rgMenuActionsSelected = []; } // when ripgrep actions are available show preliminary quickpick for those options to add to the query function setupRgMenuActions() { reset(); - - qp.placeholder = '🫧 Select actions or type custom rg options (Space key to check/uncheck)'; - qp.canSelectMany = true; + cx.qp.placeholder = '🫧 Select actions or type custom rg options (Space key to check/uncheck)'; + cx.qp.canSelectMany = true; // add items from the config - qp.items = config.rgMenuActions.map(({value, label}) => ({ + cx.qp.items = cx.config.rgMenuActions.map(({value, label}) => ({ _type: 'QuickPickItemRgMenuAction', label: label ?? value, description: label ? value : undefined, @@ -80,34 +70,34 @@ export const periscope = () => { ); function next() { - rgMenuActionsSelected = (qp.selectedItems as QPItemRgMenuAction[]).map(item => item.data.rgOption); + cx.rgMenuActionsSelected = (cx.qp.selectedItems as QPItemRgMenuAction[]).map(item => item.data.rgOption); // if no actions selected, then use the current query as a custom command to rg - if (!rgMenuActionsSelected.length && qp.value) { - rgMenuActionsSelected.push(qp.value); - qp.value = ''; + if (!cx.rgMenuActionsSelected.length && cx.qp.value) { + cx.rgMenuActionsSelected.push(cx.qp.value); + cx.qp.value = ''; } setupQuickPickForQuery(); } - disposables.rgMenuActions.push( - qp.onDidTriggerButton(next), - qp.onDidAccept(next) + cx.disposables.rgMenuActions.push( + cx.qp.onDidTriggerButton(next), + cx.qp.onDidAccept(next) ); } // update quickpick event listeners for the query function setupQuickPickForQuery() { - qp.placeholder = '🫧'; - qp.items = []; - qp.canSelectMany = false; - qp.value = getSelectedText(); - disposables.query.push( - qp.onDidChangeValue(onDidChangeValue), - qp.onDidChangeActive(onDidChangeActive), - qp.onDidAccept(onDidAccept), - qp.onDidTriggerItemButton(onDidTriggerItemButton) + cx.qp.placeholder = '🫧'; + cx.qp.items = []; + cx.qp.canSelectMany = false; + cx.qp.value = getSelectedText(); + cx.disposables.query.push( + cx.qp.onDidChangeValue(onDidChangeValue), + cx.qp.onDidChangeActive(onDidChangeActive), + cx.qp.onDidAccept(onDidAccept), + cx.qp.onDidTriggerItemButton(onDidTriggerItemButton) ); } @@ -122,12 +112,12 @@ export const periscope = () => { checkKillProcess(); if (value) { - query = value; + cx.query = value; // Jump to rg menu actions if ( - config.gotoRgMenuActionsPrefix && - value.startsWith(config.gotoRgMenuActionsPrefix) + cx.config.gotoRgMenuActionsPrefix && + value.startsWith(cx.config.gotoRgMenuActionsPrefix) ) { setupRgMenuActions(); return; @@ -135,20 +125,20 @@ export const periscope = () => { // Jump to native vscode search option if ( - config.enableGotoNativeSearch && - config.gotoNativeSearchSuffix && - value.endsWith(config.gotoNativeSearchSuffix) + cx.config.enableGotoNativeSearch && + cx.config.gotoNativeSearchSuffix && + value.endsWith(cx.config.gotoNativeSearchSuffix) ) { - openNativeVscodeSearch(query, qp); + openNativeVscodeSearch(cx.query, cx.qp); return; } - if(config.rgQueryParams.length > 0) { + if(cx.config.rgQueryParams.length > 0) { const { newQuery, extraRgFlags } = extraRgFlagsFromQuery(value); - query = newQuery; // update query for later use + cx.query = newQuery; // update query for later use - if(config.rgQueryParamsShowTitle) { // update title with preview - qp.title = extraRgFlags.length > 0 ? `rg '${query}' ${extraRgFlags.join(' ')}` : undefined; + if(cx.config.rgQueryParamsShowTitle) { // update title with preview + cx.qp.title = extraRgFlags.length > 0 ? `rg '${cx.query}' ${extraRgFlags.join(' ')}` : undefined; } search(newQuery, extraRgFlags); @@ -156,7 +146,7 @@ export const periscope = () => { search(value); } } else { - qp.items = []; + cx.qp.items = []; } } @@ -180,7 +170,7 @@ export const periscope = () => { // when prompt is 'CANCELLED' function onDidHide() { - if (!qp.selectedItems[0]) { + if (!cx.qp.selectedItems[0]) { if (previousActiveEditor) { vscode.window.showTextDocument( previousActiveEditor.document, @@ -193,14 +183,14 @@ export const periscope = () => { } function search(value: string, rgExtraFlags?: string[]) { - qp.busy = true; + cx.qp.busy = true; const rgCmd = rgCommand(value, rgExtraFlags); log('rgCmd:', rgCmd); checkKillProcess(); let searchResults: any[] = []; const spawnProcess = spawn(rgCmd, [], { shell: true }); - spawnRegistry.push(spawnProcess); + cx.spawnRegistry.push(spawnProcess); spawnProcess.stdout.on('data', (data: Buffer) => { const lines = data.toString().split('\n').filter(Boolean); @@ -228,11 +218,19 @@ export const periscope = () => { }); spawnProcess.stderr.on('data', (data: Buffer) => { + const errorMsg = data.toString(); + + // additional UI feedback for common errors + if (errorMsg.includes('unrecognized')) { + cx.qp.title = errorMsg; + } + log.error(data.toString()); + handleNoResultsFound(); }); spawnProcess.on('exit', (code: number) => { if (code === 0 && searchResults.length) { - qp.items = searchResults + cx.qp.items = searchResults .map(searchResult => { // break the filename via regext ':line:col:' const {filePath, linePos, colPos, textResult} = searchResult; @@ -259,7 +257,6 @@ export const periscope = () => { notifyError(`PERISCOPE: Ripgrep exited with code ${code} (Ripgrep not found. Please install ripgrep)`); } else if (code === 1) { log(`Ripgrep exited with code ${code} (no results found)`); - notifyError(`Ripgrep exited with code ${code} (no results found)`); handleNoResultsFound(); } else if (code === 2) { log.error(`Ripgrep exited with code ${code} (error during search operation)`); @@ -268,17 +265,17 @@ export const periscope = () => { log.error(msg); notifyError(`PERISCOPE: ${msg}`); } - qp.busy = false; + cx.qp.busy = false; }); } function handleNoResultsFound() { - if (config.showPreviousResultsWhenNoMatches) { + if (cx.config.showPreviousResultsWhenNoMatches) { return; } // hide the previous results if no results found - qp.items = []; + cx.qp.items = []; // no peek preview available, show the origin document instead showPreviewOfOriginDocument(); } @@ -292,6 +289,7 @@ export const periscope = () => { } function checkKillProcess() { + const {spawnRegistry} = cx; spawnRegistry.forEach(spawnProcess => { spawnProcess.stdout.destroy(); spawnProcess.stderr.destroy(); @@ -299,44 +297,11 @@ export const periscope = () => { }); // check if spawn process is no longer running and if so remove from registry - spawnRegistry = spawnRegistry.filter(spawnProcess => { + cx.spawnRegistry = spawnRegistry.filter(spawnProcess => { return !spawnProcess.killed; }); } - function rgCommand(value: string, extraFlags?: string[]) { - const rgPath = ripgrepPath(config.rgPath); - - const rgRequiredFlags = [ - '--line-number', - '--column', - '--no-heading', - '--with-filename', - '--color=never', - '--json' - ]; - - const rootPaths = workspaceFolders - ? workspaceFolders.map(folder => folder.uri.fsPath) - : []; - - const excludes = config.rgGlobExcludes.map(exclude => { - return `--glob "!${exclude}"`; - }); - - const rgFlags = [ - ...rgRequiredFlags, - ...config.rgOptions, - ...rgMenuActionsSelected, - ...rootPaths, - ...config.addSrcPaths, - ...(extraFlags || []), - ...excludes, - ]; - - return `"${rgPath}" "${value}" ${rgFlags.join(' ')}`; - } - // extract rg flags from the query, can match multiple regex's function extraRgFlagsFromQuery(query: string): { newQuery: string; @@ -345,7 +310,7 @@ export const periscope = () => { const extraRgFlags: string[] = []; const queries = [query]; - for (const { param, regex } of config.rgQueryParams) { + for (const { param, regex } of cx.config.rgQueryParams) { if (param && regex) { const match = query.match(regex); if (match && match.length > 1) { @@ -390,7 +355,7 @@ export const periscope = () => { function accept(item?: QPItemQuery) { checkKillProcess(); - const currentItem = item ? item : qp.selectedItems[0] as QPItemQuery; + const currentItem = item ? item : cx.qp.selectedItems[0] as QPItemQuery; if (!currentItem?.data) { return; } @@ -406,12 +371,11 @@ export const periscope = () => { vscode.window.showTextDocument(document, options).then(editor => { setCursorPosition(editor, linePos, colPos); - qp.dispose(); + cx.qp.dispose(); }); }); } - // required to update the quick pick item with result information function createResultItem( filePath: string, @@ -442,39 +406,9 @@ export const periscope = () => { }; } - function formatPathLabel(filePath: string) { - if (!workspaceFolders) { - return filePath; - } - - // find correct workspace folder - let workspaceFolder = workspaceFolders[0]; - for (const folder of workspaceFolders) { - if (filePath.startsWith(folder.uri.fsPath)) { - workspaceFolder = folder; - break; - } - } - - const workspaceFolderName = workspaceFolder.name; - const relativeFilePath = path.relative(workspaceFolder.uri.fsPath, filePath); - const folders = [workspaceFolderName, ...relativeFilePath.split(path.sep)]; - - // abbreviate path if too long - if ( - folders.length > - config.startFolderDisplayDepth + config.endFolderDisplayDepth - ) { - const initialFolders = folders.splice(config.startFolderDisplayIndex, config.startFolderDisplayDepth); - folders.splice(0, folders.length - config.endFolderDisplayDepth); - folders.unshift(...initialFolders, '...'); - } - return folders.join(path.sep); - } - function finished() { checkKillProcess(); - highlightLineDecorationType().remove(); + cx.highlightDecoration.remove(); setActiveContext(false); disposeAll(); updateActiveQP(undefined); diff --git a/src/lib/ripgrep.ts b/src/lib/ripgrep.ts new file mode 100644 index 0000000..7bc1904 --- /dev/null +++ b/src/lib/ripgrep.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; +import { getConfig } from "../utils/getConfig"; +import { ripgrepPath } from "../utils/ripgrep"; +import { context } from './context'; + +export function rgCommand(value: string, extraFlags?: string[]) { + let config = getConfig(); + let workspaceFolders = vscode.workspace.workspaceFolders; + + const rgPath = ripgrepPath(config.rgPath); + + const rgRequiredFlags = [ + '--line-number', + '--column', + '--no-heading', + '--with-filename', + '--color=never', + '--json' + ]; + + const rootPaths = workspaceFolders + ? workspaceFolders.map(folder => folder.uri.fsPath) + : []; + + const excludes = config.rgGlobExcludes.map(exclude => { + return `--glob "!${exclude}"`; + }); + + const rgFlags = [ + ...rgRequiredFlags, + ...config.rgOptions, + ...context.rgMenuActionsSelected, + ...rootPaths, + ...config.addSrcPaths, + ...(extraFlags || []), + ...excludes, + ]; + + return `"${rgPath}" "${value}" ${rgFlags.join(' ')}`; +} \ No newline at end of file diff --git a/src/utils/formatPathLabel.ts b/src/utils/formatPathLabel.ts new file mode 100644 index 0000000..302eb36 --- /dev/null +++ b/src/utils/formatPathLabel.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { getConfig } from './getConfig'; + +/** + * Util to improve formatting of file paths + * provides control to abbreviate paths that are too long + * exposes initial folder display depth and end folder display depth + * workspace folder name is displayed at the start of the path to provide additional context when necessary + */ +export function formatPathLabel(filePath: string) { + let workspaceFolders = vscode.workspace.workspaceFolders; + let config = getConfig(); + + if (!workspaceFolders) { + return filePath; + } + + // find correct workspace folder + let workspaceFolder = workspaceFolders[0]; + for (const folder of workspaceFolders) { + if (filePath.startsWith(folder.uri.fsPath)) { + workspaceFolder = folder; + break; + } + } + + const workspaceFolderName = workspaceFolder.name; + const relativeFilePath = path.relative(workspaceFolder.uri.fsPath, filePath); + const folders = [workspaceFolderName, ...relativeFilePath.split(path.sep)]; + + // abbreviate path if too long + if ( + folders.length > + config.startFolderDisplayDepth + config.endFolderDisplayDepth + ) { + const initialFolders = folders.splice(config.startFolderDisplayIndex, config.startFolderDisplayDepth); + folders.splice(0, folders.length - config.endFolderDisplayDepth); + folders.unshift(...initialFolders, '...'); + } + return folders.join(path.sep); +} diff --git a/src/utils/decorationType.ts b/src/utils/highlightLineDecorationType.ts similarity index 100% rename from src/utils/decorationType.ts rename to src/utils/highlightLineDecorationType.ts