Skip to content

Commit

Permalink
feat: new commands to add files to context
Browse files Browse the repository at this point in the history
Add appmap.addToContext and appmap.explorer.addToContext to allow the
user to pin files in the Chat context.
  • Loading branch information
apotterri committed Sep 13, 2024
1 parent 223784e commit 1c1d33e
Show file tree
Hide file tree
Showing 9 changed files with 928 additions and 694 deletions.
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,18 @@
{
"command": "appmap.openAIApiKey.status",
"title": "AppMap: Check OpenAI API Key Status"
},
{
"command": "appmap.editor.title.addToContext",
"title": "AppMap: Add File To Context"
},
{
"command": "appmap.explorer.addToContext",
"title": "AppMap: Add Files To Context"
},
{
"command": "appmap.addToContext",
"title": "AppMap: Add Files To Context"
}
],
"configuration": {
Expand Down Expand Up @@ -307,6 +319,11 @@
"type": "boolean",
"default": false,
"description": "Enable AppMap scanner"
},
"appMap.maxPinnedFileSizeKB": {
"type": "number",
"default": 20,
"description": "Maximum size of a file (in KB) that can be pinned to the Navie context"
}
}
},
Expand Down Expand Up @@ -501,6 +518,30 @@
{
"command": "appmap.context.deleteAppMaps",
"when": "false"
},
{
"command": "appmap.editor.title.addToContext",
"when": "false"
},
{
"command": "appmap.explorer.addToContext",
"when": "false"
},
{
"command": "appmap.addToContext",
"when": "activeWebviewPanelId=='chatSearch'"
}
],
"editor/title/context": [
{
"command": "appmap.editor.title.addToContext",
"when": "activeWebviewPanelId=='chatSearch' && resourceScheme == 'file'"
}
],
"explorer/context": [
{
"command": "appmap.explorer.addToContext",
"when": "activeWebviewPanelId=='chatSearch'"
}
],
"view/title": [
Expand Down
51 changes: 51 additions & 0 deletions src/commands/addToContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { PinFileRequest } from '@appland/components';
import * as vscode from 'vscode';
import ChatSearchWebview from '../webviews/chatSearchWebview';

export default function addToContext(
context: vscode.ExtensionContext,
chatSearchWebview: Promise<ChatSearchWebview>
) {
const currentWebview = async (): Promise<vscode.Webview | undefined> =>
(await chatSearchWebview).currentWebview;

context.subscriptions.push(
vscode.commands.registerCommand(
'appmap.explorer.addToContext',
(_item, selected: vscode.Uri[]) => {
fetchFiles(selected);
}
)
);

context.subscriptions.push(
vscode.commands.registerCommand('appmap.editor.title.addToContext', (item) => {
fetchFiles([item]);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('appmap.addToContext', async () => {
const webview = await currentWebview();
if (!webview) return;

webview.postMessage({ type: 'choose-files-to-pin' });
})
);

const fetchFiles = async (uris: vscode.Uri[]) => {
const webview = await currentWebview();
if (!webview) return;

const requests: PinFileRequest[] = uris.map((u) => {
const name = u.path.split('/').slice(-1)[0];
return {
name,
uri: u.toString(),
};
});
webview.postMessage({
type: 'fetch-pinned-files',
requests,
});
};
}
5 changes: 5 additions & 0 deletions src/configuration/extensionSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,9 @@ export default class ExtensionSettings {
public static bindContext(): void {
vscode.commands.executeCommand('setContext', 'appmap.scannerEnabled', this.scannerEnabled);
}

public static get maxPinnedFileSizeKB(): number {
const ret = vscode.workspace.getConfiguration('appMap').get('maxPinnedFileSizeKB') as number;
return ret !== undefined ? ret : 20000;
}
}
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from 'vscode';
import assert from 'assert';
import RemoteRecording from './actions/remoteRecording';
import AppMapService from './appMapService';
import addToContext from './commands/addToContext';
import deleteAllAppMaps from './commands/deleteAllAppMaps';
import registerInspectCodeObject from './commands/inspectCodeObject';
import registerSequenceDiagram from './commands/sequenceDiagram';
Expand Down Expand Up @@ -277,6 +278,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<AppMap
findByName(context, appmapCollectionFile);

appmapState(context, editorProvider, chatSearchWebview);
addToContext(context, chatSearchWebview);
quickSearch(context);
resetUsageState(context, extensionState);
downloadLatestJavaJar(context);
Expand Down
78 changes: 78 additions & 0 deletions src/webviews/chatSearchWebview.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PinFileRequest } from '@appland/components';
import assert from 'assert';
import * as vscode from 'vscode';
import getWebviewContent from './getWebviewContent';
import appmapMessageHandler from './appmapMessageHandler';
Expand Down Expand Up @@ -56,6 +58,61 @@ export default class ChatSearchWebview {
return this.webviewList.currentWebview;
}

async doPinFiles(panel: vscode.WebviewPanel, reqs: PinFileRequest[]) {
const maxPinnedFileSize = ExtensionSettings.maxPinnedFileSizeKB * 1024;
type PinFileRequestWithLength = PinFileRequest & { contentLength?: number };
const requests = await Promise.all(
reqs.map(async (r: PinFileRequest): Promise<PinFileRequestWithLength> => {
let contentLength = r.content?.length;
if (!r.content) {
assert(r.uri, `doPinFiles, ${r.name}: no content, no uri`);
const u: vscode.Uri = vscode.Uri.parse(r.uri);
const stat = await vscode.workspace.fs.stat(u);
contentLength = stat.size;
if (contentLength < maxPinnedFileSize) {
const doc = await vscode.workspace.openTextDocument(u);
r.content = doc.getText();
}
}

return {
name: r.name,
uri: r.uri,
content: r.content,
contentLength,
};
})
);

const goodRequests = requests.filter((r: PinFileRequestWithLength) => {
if (r.contentLength > maxPinnedFileSize) {
const setting = ExtensionSettings.maxPinnedFileSizeKB;
const len = Math.round(r.contentLength / 1024);
vscode.window
.showErrorMessage(
`${r.name} (${len}KB) exceeds the maximum file size of ${setting}KB.`,
'Change',
'Cancel'
)
.then((answer) => {
if (answer === 'Change') {
vscode.commands.executeCommand(
'workbench.action.openSettings',
'appmap.maxPinnedFileSizeKB'
);
}
});
return false;
}
return true;
});

if (goodRequests.length > 0) {
const msg = { type: 'pin-files', requests: goodRequests.map((r) => new PinFileRequest(r)) };
panel.webview.postMessage(msg);
}
}

async explain({
workspace,
codeSelection,
Expand Down Expand Up @@ -190,6 +247,23 @@ export default class ChatSearchWebview {
}
break;
}
case 'choose-files-to-pin': {
await this.chooseFilesToPin().then(async (uris: vscode.Uri[] | undefined) => {
if (!uris) return;
const requests: PinFileRequest[] = uris.map((u) => ({
name: u.toString(),
uri: u.toString(),
}));
this.doPinFiles(panel, requests);
});
break;
}

case 'fetch-pinned-files': {
const { requests } = message;
this.doPinFiles(panel, requests);
break;
}
}
});

Expand All @@ -199,6 +273,10 @@ export default class ChatSearchWebview {
};
}

async chooseFilesToPin() {
return vscode.window.showOpenDialog({ canSelectMany: true });
}

updateFilters(savedFilters: SavedFilter[]) {
this.webviewList.webviews.forEach((webview) => {
webview.postMessage({
Expand Down
120 changes: 120 additions & 0 deletions test/integration/chat/chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import assert from 'assert';
import { promises as fs } from 'fs';
import sinon from 'sinon';

import path from 'path';
import * as vscode from 'vscode';
import { initializeWorkspaceServices } from '../../../src/services/workspaceServices';
import ChatSearchWebview from '../../../src/webviews/chatSearchWebview';
import { initializeWorkspace, waitForExtension, withAuthenticatedUser, withTmpDir } from '../util';

type PinFileEvent = {
type: string;
location: string;
content: string;
};

describe('chat', () => {
let sandbox: sinon.SinonSandbox;
let chatSearchWebview: ChatSearchWebview;

withAuthenticatedUser();

before(async () => {
await initializeWorkspace();
const extension = await waitForExtension();
chatSearchWebview = await extension.chatSearchWebview;
initializeWorkspaceServices();
});

beforeEach(() => (sandbox = sinon.createSandbox()));
afterEach(async () => {
vscode.commands.executeCommand('workbench.action.closeActiveEditor');
sandbox.restore();
});

describe('appmap.explain', () => {
it('opens a Chat + Search view', async () => {
await vscode.commands.executeCommand('appmap.explain');
assert(chatSearchWebview.currentWebview);
});
});

describe('once the Chat is opened', () => {
let chatView: vscode.Webview;

beforeEach(async () => {
await vscode.commands.executeCommand('appmap.explain');
const v = chatSearchWebview.currentWebview;
assert(v);

await new Promise<void>((resolve) => {
v.onDidReceiveMessage(async (msg) => {
if (msg.command === 'chat-search-ready') {
resolve();
}
});
});
chatView = v;
});

const waitForPin = () =>
new Promise<PinFileEvent>((resolve) => {
chatView.onDidReceiveMessage(async (msg) => {
if (msg.command === 'pin') {
resolve(msg.event);
}
});
});

const expectedContent = 'Hello World!';
const newTmpFiles = async (tmpDir: string) => {
const fullPath = path.join(tmpDir, 'hello-world.txt');
await fs.writeFile(fullPath, expectedContent, 'utf-8');
return [vscode.Uri.file(fullPath)];
};

const verifyPinEvent = (event: PinFileEvent, files: vscode.Uri[]) => {
assert.strictEqual(event.type, 'file');
assert.strictEqual(event.location, files[0].path);
assert.strictEqual(event.content, expectedContent);
};

describe('appmap.addToContext', () => {
it('pins a file', async () => {
return withTmpDir(async (tmpDir) => {
const files = await newTmpFiles(tmpDir);
sandbox.stub(chatSearchWebview, 'chooseFilesToPin').resolves(files);
const p = waitForPin();
await vscode.commands.executeCommand('appmap.addToContext');
const event = await p;
verifyPinEvent(event, files);
});
});
});

describe('appmap.explorer.addToContext', () => {
it('pins a file', async () => {
return withTmpDir(async (tmpDir) => {
const files = await newTmpFiles(tmpDir);
const p = waitForPin();
await vscode.commands.executeCommand('appmap.explorer.addToContext', null, files);
const event = await p;
verifyPinEvent(event, files);
});
});
});

describe('appmap.editor.title.addToContext', () => {
it('pins a file', async () => {
return withTmpDir(async (tmpDir) => {
const files = await newTmpFiles(tmpDir);
const p = waitForPin();
await vscode.commands.executeCommand('appmap.editor.title.addToContext', files[0]);
const event = await p;
verifyPinEvent(event, files);
});
});
});
});
});
25 changes: 0 additions & 25 deletions test/integration/explain/explain.test.ts

This file was deleted.

Loading

0 comments on commit 1c1d33e

Please sign in to comment.