Skip to content

Commit

Permalink
Merge pull request #1015 from getappmap/pin-files_20240905
Browse files Browse the repository at this point in the history
Add the ability to pin files to the Navie Context
  • Loading branch information
apotterri authored Sep 13, 2024
2 parents 353ee6f + 0ab9251 commit 1c456b5
Show file tree
Hide file tree
Showing 9 changed files with 936 additions and 670 deletions.
43 changes: 42 additions & 1 deletion 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 Expand Up @@ -654,7 +695,7 @@
"dependencies": {
"@appland/appmap": "^3.129.0",
"@appland/client": "^1.14.1",
"@appland/components": "^4.35.0",
"@appland/components": "^4.37.0",
"@appland/diagrams": "^1.8.0",
"@appland/models": "^2.10.2",
"@appland/rpc": "^1.15.0",
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 @@ -279,6 +280,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 1c456b5

Please sign in to comment.