Skip to content

Commit

Permalink
feat: User is prompted to choose a directory for Explain command
Browse files Browse the repository at this point in the history
  • Loading branch information
kgilpin committed Jan 24, 2024
1 parent 79b4dd9 commit 0f371fe
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 52 deletions.
91 changes: 91 additions & 0 deletions src/lib/selectIndexProcess.ts
Original file line number Diff line number Diff line change
@@ -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<IndexProcess | ReasonCode | undefined> {
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,
};
}
1 change: 0 additions & 1 deletion src/services/indexProcessWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as vscode from 'vscode';
import ExtensionSettings from '../configuration/extensionSettings';
import { NodeProcessService } from './nodeProcessService';
import { ProcessId, ProcessWatcher, ProcessWatcherOptions } from './processWatcher';
Expand Down
73 changes: 23 additions & 50 deletions src/webviews/chatSearchWebview.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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<string | undefined> => {
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',
Expand Down
9 changes: 8 additions & 1 deletion test/integration/explain/explain.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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');
Expand Down
106 changes: 106 additions & 0 deletions test/unit/lib/selectIndexProcess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect } from 'chai';
import sinon from 'sinon';
import '../mock/vscode';
import * as vscode from '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,
});
});
});
});
describe('when there are multiple index processes', () => {
beforeEach(() => {
processes = [
{
id: ProcessId.Index,
isRpcAvailable: sandbox.stub().returns(true),
configFolder: './project-a',
rpcPort: 1,
} as unknown as IndexProcessWatcher,
{
id: ProcessId.Index,
isRpcAvailable: sandbox.stub().returns(true),
configFolder: './project-b',
rpcPort: 2,
} as unknown as IndexProcessWatcher,
];
});

it('prompts the user to choose', async () => {
const quickPick = sandbox.stub(vscode.window, 'showQuickPick');
quickPick.onFirstCall().resolves({
label: './project-a',
watcher: processes[0],
} as unknown as vscode.QuickPickItem & { watcher: IndexProcessWatcher });

const result = await selectIndexProcess();
expect(result).to.deep.equal({
configFolder: './project-a',
rpcPort: 1,
});
});
});
});
1 change: 1 addition & 0 deletions test/unit/mock/vscode/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default {
showInputBox() {
return '';
},
showQuickPick: doNothing,
showErrorMessage: doNothing,
showInformationMessage: doNothing,
workspaceFolders: [],
Expand Down

0 comments on commit 0f371fe

Please sign in to comment.