From bf5a35d4f611ecdb2e7ff4e90515c2b213dd171b Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Tue, 23 Jan 2024 16:05:29 -0500 Subject: [PATCH] feat: User is prompted to choose a directory for Explain command --- src/lib/selectIndexProcess.ts | 91 ++++++++++++++++++++++++ src/services/indexProcessWatcher.ts | 1 - src/webviews/chatSearchWebview.ts | 73 ++++++------------- test/integration/explain/explain.test.ts | 9 ++- test/unit/lib/selectIndexProcess.test.ts | 73 +++++++++++++++++++ 5 files changed, 195 insertions(+), 52 deletions(-) create mode 100644 src/lib/selectIndexProcess.ts create mode 100644 test/unit/lib/selectIndexProcess.test.ts diff --git a/src/lib/selectIndexProcess.ts b/src/lib/selectIndexProcess.ts new file mode 100644 index 000000000..57726c260 --- /dev/null +++ b/src/lib/selectIndexProcess.ts @@ -0,0 +1,91 @@ +import * as vscode from 'vscode'; +import IndexProcessWatcher from '../services/indexProcessWatcher'; +import { NodeProcessService } from '../services/nodeProcessService'; +import { workspaceServices } from '../services/workspaceServices'; + +export type IndexProcess = { + configFolder: string; + rpcPort: number; +}; + +export enum ReasonCode { + NoIndexProcessWatchers = 1, + NoReadyIndexProcessWatchers = 2, + NoSelectionMade = 3, +} + +export function readyProcessWatchers( + workspace?: vscode.WorkspaceFolder +): IndexProcessWatcher[] | undefined { + const nodeProcessService = workspaceServices().getService(NodeProcessService); + if (!nodeProcessService) { + console.warn('No node process service found'); + return; + } + + const nodeProcessServices = workspaceServices().getServiceInstances(nodeProcessService); + if (nodeProcessServices.length === 0) { + console.log('No node process services found'); + return; + } + + const indexProcessWatchers = nodeProcessServices + .filter((service) => workspace === undefined || service.folder === workspace) + .flatMap( + (service) => + service.processes.filter((process) => process.id === 'index') as IndexProcessWatcher[] + ); + + return indexProcessWatchers; +} + +export default async function selectIndexProcess( + workspace?: vscode.WorkspaceFolder +): Promise { + const indexProcessWatchers = readyProcessWatchers(workspace); + if (!indexProcessWatchers) { + console.warn(`No index process watchers found for ${workspace?.name || 'your workspace'}`); + return; + } + + if (indexProcessWatchers.length === 0) { + console.log('No index process watchers found'); + return ReasonCode.NoIndexProcessWatchers; + } + + const readyIndexProcessWatchers = indexProcessWatchers.filter((watcher) => + watcher.isRpcAvailable() + ); + if (readyIndexProcessWatchers.length === 0) { + console.log('No ready index process watchers found'); + return ReasonCode.NoReadyIndexProcessWatchers; + } + + let selectedWatcher: IndexProcessWatcher | undefined; + if (readyIndexProcessWatchers.length === 1) { + selectedWatcher = readyIndexProcessWatchers[0]; + } else { + const pickResult = await vscode.window.showQuickPick( + readyIndexProcessWatchers.map((watcher) => ({ + label: watcher.configFolder, + watcher, + })), + { + placeHolder: 'Select a project directory for your question', + } + ); + if (!pickResult) return ReasonCode.NoSelectionMade; + + selectedWatcher = pickResult.watcher; + } + + if (!selectedWatcher.rpcPort) { + console.warn(`No RPC port available on index process watcher ${selectedWatcher.configFolder}`); + return; + } + + return { + configFolder: selectedWatcher.configFolder, + rpcPort: selectedWatcher.rpcPort, + }; +} diff --git a/src/services/indexProcessWatcher.ts b/src/services/indexProcessWatcher.ts index bf5831231..45af67bd2 100644 --- a/src/services/indexProcessWatcher.ts +++ b/src/services/indexProcessWatcher.ts @@ -1,4 +1,3 @@ -import * as vscode from 'vscode'; import ExtensionSettings from '../configuration/extensionSettings'; import { NodeProcessService } from './nodeProcessService'; import { ProcessId, ProcessWatcher, ProcessWatcherOptions } from './processWatcher'; diff --git a/src/webviews/chatSearchWebview.ts b/src/webviews/chatSearchWebview.ts index 490605a0a..be7c39324 100644 --- a/src/webviews/chatSearchWebview.ts +++ b/src/webviews/chatSearchWebview.ts @@ -1,13 +1,9 @@ import * as vscode from 'vscode'; import getWebviewContent from './getWebviewContent'; -import { workspaceServices } from '../services/workspaceServices'; -import { NodeProcessService } from '../services/nodeProcessService'; -import { warn } from 'console'; -import IndexProcessWatcher from '../services/indexProcessWatcher'; -import { ProcessId } from '../services/processWatcher'; import appmapMessageHandler from './appmapMessageHandler'; import FilterStore, { SavedFilter } from './filterStore'; import WebviewList from './WebviewList'; +import selectIndexProcess, { IndexProcess, ReasonCode } from '../lib/selectIndexProcess'; export default class ChatSearchWebview { private webviewList = new WebviewList(); @@ -27,54 +23,31 @@ export default class ChatSearchWebview { return this.webviewList.currentWebview; } - readyIndexProcess(workspace: vscode.WorkspaceFolder): IndexProcessWatcher | undefined { - const processServiceInstance = workspaceServices().getServiceInstanceFromClass( - NodeProcessService, - workspace - ); - if (!processServiceInstance) return; - - const indexProcess = processServiceInstance.processes.find( - (proc) => proc.id === ProcessId.Index - ) as IndexProcessWatcher; - if (!indexProcess) { - warn(`No ${ProcessId.Index} helper process found for workspace: ${workspace.name}`); - return; - } - - if (!indexProcess.isRpcAvailable()) return; - - return indexProcess; - } - - isReady(workspace: vscode.WorkspaceFolder): boolean { - return !!this.readyIndexProcess(workspace); - } - async explain(workspace?: vscode.WorkspaceFolder, question?: string) { - if (!workspace) { - const workspaces = vscode.workspace.workspaceFolders; - if (!workspaces) return; - - if (workspaces.length === 1) { - workspace = workspaces[0]; - } else { - workspace = await vscode.window.showWorkspaceFolderPick({ - placeHolder: 'Select a workspace folder', - }); - } - if (!workspace) return; + const selectIndexProcessResult = await selectIndexProcess(workspace); + if (!selectIndexProcessResult) return; + + let selectedWatcher: IndexProcess | undefined; + switch (selectIndexProcessResult) { + case ReasonCode.NoIndexProcessWatchers: + vscode.window.showInformationMessage( + `${workspace?.name || 'Your workspace'} does not have AppMaps` + ); + break; + case ReasonCode.NoReadyIndexProcessWatchers: + vscode.window.showInformationMessage( + `AppMap AI is not ready yet. Please try again in a few seconds.` + ); + break; + case ReasonCode.NoSelectionMade: + break; + default: + selectedWatcher = selectIndexProcessResult; + break; } + if (!selectedWatcher) return; - const showError = async (message: string): Promise => { - return vscode.window.showErrorMessage(message); - }; - - const indexProcess = this.readyIndexProcess(workspace); - if (!indexProcess) - return showError('AppMap Explain is not ready yet. Please try again in a few seconds.'); - - const { rpcPort: appmapRpcPort } = indexProcess; + const { rpcPort: appmapRpcPort } = selectedWatcher; const panel = vscode.window.createWebviewPanel( 'chatSearch', diff --git a/test/integration/explain/explain.test.ts b/test/integration/explain/explain.test.ts index 2db049b4e..962fd0286 100644 --- a/test/integration/explain/explain.test.ts +++ b/test/integration/explain/explain.test.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; import { initializeWorkspace, waitFor, waitForExtension, withAuthenticatedUser } from '../util'; import assert from 'assert'; +import { readyProcessWatchers } from '../../../src/lib/selectIndexProcess'; +import { initializeWorkspaceServices } from '../../../src/services/workspaceServices'; describe('appmap.explain', () => { withAuthenticatedUser(); @@ -10,12 +12,17 @@ describe('appmap.explain', () => { afterEach(initializeWorkspace); it('opens a Chat + Search view', async () => { + initializeWorkspaceServices(); + const chatSearchWebview = (await waitForExtension()).chatSearchWebview; const workspace = vscode.workspace.workspaceFolders?.[0]; assert(workspace); - await waitFor('Explain service is not ready', () => chatSearchWebview.isReady(workspace)); + await waitFor( + 'Explain service is not ready', + () => readyProcessWatchers(workspace)?.length !== 0 + ); await waitFor('Invoking appmap.explain opens a new text document', async () => { await vscode.commands.executeCommand('appmap.explain'); diff --git a/test/unit/lib/selectIndexProcess.test.ts b/test/unit/lib/selectIndexProcess.test.ts new file mode 100644 index 000000000..cf6454af0 --- /dev/null +++ b/test/unit/lib/selectIndexProcess.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import '../mock/vscode'; +import * as workspaceServices from '../../../src/services/workspaceServices'; +import selectIndexProcess, { ReasonCode } from '../../../src/lib/selectIndexProcess'; +import { ProcessId } from '../../../src/services/processWatcher'; +import IndexProcessWatcher from '../../../src/services/indexProcessWatcher'; + +describe('selectIndexProcess', () => { + let sandbox: sinon.SinonSandbox; + let processes: IndexProcessWatcher[]; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + processes = []; + const stubService = sandbox.stub(); + sandbox.stub(workspaceServices, 'workspaceServices').returns({ + getService: sandbox.stub().returns(stubService), + getServiceInstances: (service: unknown) => { + expect(service).to.eq(stubService); + return [{ processes }]; + }, + } as unknown as workspaceServices.WorkspaceServices); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('when there is no index process', () => { + it('returns undefined', async () => { + const result = await selectIndexProcess(); + expect(result).to.eq(ReasonCode.NoIndexProcessWatchers); + }); + }); + describe('when there is one index process', () => { + describe('but the RPC port is not available', () => { + beforeEach(() => { + processes = [ + { + id: ProcessId.Index, + isRpcAvailable: sandbox.stub().returns(false), + } as unknown as IndexProcessWatcher, + ]; + }); + + it('returns undefined', async () => { + const result = await selectIndexProcess(); + expect(result).to.eq(ReasonCode.NoReadyIndexProcessWatchers); + }); + }); + describe('and the RPC port is available', () => { + beforeEach(() => { + processes = [ + { + id: ProcessId.Index, + isRpcAvailable: sandbox.stub().returns(true), + configFolder: './the-project', + rpcPort: 1234, + } as unknown as IndexProcessWatcher, + ]; + }); + + it('returns the process', async () => { + const result = await selectIndexProcess(); + expect(result).to.deep.equal({ + configFolder: './the-project', + rpcPort: 1234, + }); + }); + }); + }); +});