Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Search + Explain with Navie #845

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0d4a4c3
feat: Detect RPC server port on startup
kgilpin Nov 20, 2023
0e916b9
feat: 'Search AppMaps' Code Action
kgilpin Nov 20, 2023
627d442
feat: Explain the first search match
kgilpin Nov 21, 2023
da1b7fe
feat: Use symbol provider API to lookup source code
kgilpin Nov 22, 2023
7338f55
refactor: Remove comments and temp files
kgilpin Nov 22, 2023
b96160c
drop: Workspace dependency to @appland/rpc
kgilpin Nov 22, 2023
cc13be4
feat: Provide free-form 'Search' command and add additional context
kgilpin Nov 22, 2023
36791ac
feat: Show progress for search
kgilpin Nov 24, 2023
e99b90e
refactor: Render PlantUML on the rpc server side
kgilpin Nov 24, 2023
38a08a0
chore: Remove development resolutions to @appland packages
kgilpin Nov 27, 2023
d0c9e06
feat: Obtain suggested vector search terms from GPT-4
kgilpin Nov 27, 2023
0fa9b20
revertme: Remove CSP from webview
kgilpin Dec 11, 2023
1d5bba0
feat: Replace Markdown search with ChatSearch
kgilpin Dec 11, 2023
ed89347
fix: Webview title
kgilpin Dec 13, 2023
3c5bb74
feat: Provide Search AppMaps from the light bulb
kgilpin Dec 13, 2023
5594eb1
refactor: Remove older search code
kgilpin Dec 13, 2023
95e6979
refactor: Rename aiPort and rpcPort
kgilpin Dec 20, 2023
1b4c7f7
refactor: Rename the command 'AppMap AI: Explain'
kgilpin Dec 21, 2023
9234745
feat: Index command gets the API key
kgilpin Dec 21, 2023
6f881c8
refactor: Remove Markdown prototype of AI Explainer
kgilpin Dec 21, 2023
8072113
feat: viewSource from AI Explain
kgilpin Dec 22, 2023
366496b
test: Add tests for appmap.quickExplain
kgilpin Dec 22, 2023
baeaf38
refactor: Rename appmap editor tests
kgilpin Dec 22, 2023
d176a17
test: Verify that appmap.explain command opens a new panel
kgilpin Dec 22, 2023
684dddb
feat: Update @appland dependencies
kgilpin Jan 3, 2024
084fcab
fix: Ensure a deleted AppMap is removed from the tree
kgilpin Jan 3, 2024
f069405
feat: Update dependencies
kgilpin Jan 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@
"command": "appmap.compareSequenceDiagrams",
"title": "AppMap: Compare Sequence Diagrams"
},
{
"command": "appmap.explain",
"title": "AppMap AI: Explain"
},
{
"command": "appmap.login",
"title": "AppMap: Login"
Expand Down Expand Up @@ -487,19 +491,23 @@
"yaml": "^2.1.1"
},
"dependencies": {
"@appland/appmap": "^3.80.2",
"@appland/client": "^1.9.0",
"@appland/components": "^3.13.2",
"@appland/diagrams": "^1.7.0",
"@appland/models": "^2.9.0",
"@appland/scanner": "^1.83.0",
"@appland/sequence-diagram": "^1.11.0",
"@appland/appmap": "^3.116.0",
"@appland/client": "^1.14.0",
"@appland/components": "^3.21.0",
"@appland/diagrams": "^1.8.0",
"@appland/models": "^2.10.0",
"@appland/rpc": "^1.0.0",
"@appland/scanner": "^1.86.0",
"@appland/sequence-diagram": "^1.12.0",
"@yarnpkg/parsers": "^3.0.0-rc.45",
"bootstrap": "^4.5.3",
"bootstrap-autocomplete": "^2.3.7",
"diff": "^5.1.0",
"handlebars": "^4.7.8",
"jayson": "^4.1.0",
"jquery": "^3.5.1",
"js-yaml": "^4.1.0",
"openai": "^4.19.1",
"popper.js": "^1.16.1",
"proper-lockfile": "^4.1.2",
"semver": "^7.3.5",
Expand All @@ -514,5 +522,17 @@
"publisherId": "f7f1004e-6038-49cd-a096-4e618fe53f77",
"isPreReleaseVersion": false
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"resolutions": {
"@appland/appmap": "portal:/Users/kgilpin/source/appland/appmap-js/packages/cli",
"@appland/client": "portal:/Users/kgilpin/source/appland/appmap-js/packages/client",
"@appland/components": "portal:/Users/kgilpin/source/appland/appmap-js/packages/components",
"@appland/diagrams": "portal:/Users/kgilpin/source/appland/appmap-js/packages/diagrams",
"@appland/models": "portal:/Users/kgilpin/source/appland/appmap-js/packages/models",
"@appland/openapi": "portal:/Users/kgilpin/source/appland/appmap-js/packages/openapi",
"@appland/rpc": "portal:/Users/kgilpin/source/appland/appmap-js/packages/rpc",
"@appland/scanner": "portal:/Users/kgilpin/source/appland/appmap-js/packages/scanner",
"@appland/sequence-diagram": "portal:/Users/kgilpin/source/appland/appmap-js/packages/sequence-diagram",
"@appland/appmap-validate": "portal:/Users/kgilpin/source/appland/appmap-js/packages/validate"
}
}
2 changes: 2 additions & 0 deletions src/appMapService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WorkspaceServices } from './services/workspaceServices';
import { AppMapTreeDataProvider } from './tree/appMapTreeDataProvider';
import { ClassMapTreeDataProvider } from './tree/classMapTreeDataProvider';
import { FindingsTreeDataProvider } from './tree/findingsTreeDataProvider';
import ChatSearchWebview from './webviews/chatSearchWebview';

export type Invocation = {
command: Command;
Expand All @@ -37,6 +38,7 @@ export type AppMapTreeDataProviders = {
export default interface AppMapService {
analysisManager: typeof AnalysisManager;
editorProvider: AppMapEditorProvider;
chatSearchWebview: ChatSearchWebview;
localAppMaps: AppMapsService;
autoIndexService: AppMapProcessService;
autoScanService: AppMapProcessService;
Expand Down
53 changes: 53 additions & 0 deletions src/commands/quickSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as vscode from 'vscode';

export class QuickSearchProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Selection | vscode.Range
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
const selectedCode = document.getText(range).trim();
if (selectedCode === '') return;

const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspaceFolder) return [];

const codeAction = new vscode.CodeAction(
'Explain with AppMap AI',
vscode.CodeActionKind.Refactor
);
codeAction.command = {
command: 'appmap.quickExplain',
title: 'Explain with AppMap AI',
arguments: [workspaceFolder.uri, selectedCode],
};
return [codeAction];
}

resolveCodeAction?(codeAction: vscode.CodeAction): vscode.ProviderResult<vscode.CodeAction> {
return codeAction;
}
}

export default function quickSearch(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
[
{ language: 'ruby' },
{ language: 'java' },
{ language: 'python' },
{ language: 'javascript' },
],
new QuickSearchProvider()
)
);

context.subscriptions.push(
vscode.commands.registerCommand(
'appmap.quickExplain',
(workspaceUri: vscode.Uri, selectedCode: string) => {
const workspace = vscode.workspace.getWorkspaceFolder(workspaceUri);
vscode.commands.executeCommand('appmap.explain', workspace, selectedCode);
}
)
);
}
31 changes: 3 additions & 28 deletions src/editor/appmapEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Telemetry, DEBUG_EXCEPTION } from '../telemetry';
import { version } from '../../package.json';
import ExtensionState from '../configuration/extensionState';
import extensionSettings from '../configuration/extensionSettings';
import { bestFilePath } from '../lib/bestFilePath';
import AppMapDocument from './AppMapDocument';
import AnalysisManager from '../services/analysisManager';
import { getStackLocations, StackLocation } from '../lib/getStackLocations';
Expand All @@ -21,6 +20,7 @@ import {
} from '../services/nodeDependencyProcess';
import { basename } from 'path';
import { readFile } from 'fs/promises';
import viewSource from '../webviews/viewSource';

export type FindingInfo = ResolvedFinding & {
stackLocations?: StackLocation[];
Expand Down Expand Up @@ -379,7 +379,7 @@ export default class AppMapEditorProvider
webviewPanel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'viewSource':
viewSource(message.text);
viewSource(message.text, document.workspaceFolder);
break;
case 'ready':
updateWebview(initialState);
Expand Down Expand Up @@ -476,38 +476,13 @@ export default class AppMapEditorProvider
if (document.workspaceFolder)
this.extensionState.setClosedAppMap(document.workspaceFolder, true);
});

function openFile(uri: vscode.Uri, lineNumber: number) {
const showOptions = {
viewColumn: vscode.ViewColumn.Beside,
selection: new vscode.Range(
new vscode.Position(lineNumber - 1, 0),
new vscode.Position(lineNumber - 1, 0)
),
};
vscode.commands.executeCommand('vscode.open', uri, showOptions);
}

async function viewSource(location: string): Promise<void> {
const match = location.match(/^(.*?)(?::(\d+))?$/);
if (!match) return;
const [, path, lineNumberStr] = match;

let lineNumber = 1;
if (lineNumberStr) {
lineNumber = Number.parseInt(lineNumberStr, 10);
}

const fileUri = await bestFilePath(path, document.workspaceFolder);
if (fileUri) openFile(fileUri, lineNumber);
}
}

/**
* Get the static html used for the editor webviews.
*/
private getHtmlForWebview(webview: vscode.Webview): string {
return getWebviewContent(webview, this.context, 'AppLand Scenario', 'app');
return getWebviewContent(webview, this.context, 'AppMap Diagram', 'app');
}

//forget usage state set by this class
Expand Down
5 changes: 5 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import getAppmapDir from './commands/getAppmapDir';
import JavaAssets from './services/javaAssets';
import checkAndTriggerFirstAppMapNotification from './lib/firstAppMapNotification';
import Watcher from './services/watcher';
import ChatSearchWebview from './webviews/chatSearchWebview';
import quickSearch from './commands/quickSearch';

export async function activate(context: vscode.ExtensionContext): Promise<AppMapService> {
Telemetry.register(context);
Expand Down Expand Up @@ -243,6 +245,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<AppMap

generateOpenApi(context);
findByName(context, appmapCollectionFile);
const chatSearchWebview = ChatSearchWebview.register(context);
quickSearch(context);
resetUsageState(context, extensionState);
updateAppMapConfigs(context, runConfigService, workspaceServices);
downloadLatestJavaJar(context);
Expand Down Expand Up @@ -270,6 +274,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<AppMap
return {
analysisManager: AnalysisManager,
editorProvider,
chatSearchWebview,
localAppMaps: appmapCollectionFile,
autoIndexService: autoIndexServiceImpl,
autoScanService: autoScanServiceImpl,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/deleteAppMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default async function deleteAppMap(
// contents, and fs.delete doesn't remove files.
console.debug(`Deleting AppMap ${uri.fsPath}`);

appMapCollection.remove(uri);

const indexDir = uri.fsPath.substring(0, uri.fsPath.lastIndexOf('.appmap.json'));

await retry(async () => await rm(`${indexDir}/metadata.json`, { force: true }));
Expand Down
2 changes: 2 additions & 0 deletions src/services/appmapCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ export default interface AppMapCollection {

has(uri: vscode.Uri): boolean;

remove(uri: vscode.Uri): void;

clear(): void;
}
10 changes: 6 additions & 4 deletions src/services/appmapCollectionFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ export default class AppMapCollectionFile implements AppMapCollection, AppMapsSe
private currentFilter = '';

async onDelete(uri: vscode.Uri): Promise<void> {
if (await fileExists(uri.fsPath)) return;

this.loaders.delete(uri.fsPath);
this.emitUpdated(uri);
if (this.loaders.delete(uri.fsPath)) this.emitUpdated(uri);
}

async onChange(uri: vscode.Uri): Promise<void> {
Expand Down Expand Up @@ -141,6 +138,11 @@ export default class AppMapCollectionFile implements AppMapCollection, AppMapsSe
return this.loaders.has(uri.fsPath);
}

// This is basically an alias for onChange.
public remove(uri: vscode.Uri): void {
this.onDelete(uri);
}

public clear(): void {
console.debug('Clearing AppMap collection from tree', this.loaders.size);
this.loaders.clear();
Expand Down
42 changes: 38 additions & 4 deletions src/services/indexProcessWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
import * as vscode from 'vscode';

import { NodeProcessService } from './nodeProcessService';
import { ConfigFileProvider, ProcessId, ProcessWatcher } from './processWatcher';
import {
ConfigFileProvider,
ProcessId,
ProcessWatcher,
ProcessWatcherOptions,
} from './processWatcher';

export default class IndexProcessWatcher extends ProcessWatcher {
public rpcPort?: number;
stdoutBuffer = '';

constructor(
configFileProvider: ConfigFileProvider,
modulePath: string,
appmapDir: string,
cwd: string,
env?: NodeJS.ProcessEnv
) {
super(configFileProvider, {
const options: ProcessWatcherOptions = {
id: ProcessId.Index,
modulePath: modulePath,
log: NodeProcessService.outputChannel,
args: ['index', '--watch', '--appmap-dir', appmapDir],
args: ['index', '--watch', '--port', '0', '--appmap-dir', appmapDir],
cwd,
env,
});
};
super(configFileProvider, options);
}

public isRpcAvailable(): boolean {
return !!this.rpcPort;
}

protected onStdout(data: string): void {
super.onStdout(data);

this.stdoutBuffer += data;
const lines = this.stdoutBuffer.split('\n');
this.stdoutBuffer = this.stdoutBuffer.slice(-100);

const portStr = lines
.map((line) => {
const match = line.match(/^Running JSON-RPC server on port: (\d+)$/);
if (match) return match[1];
})
.find(Boolean);
if (portStr) {
vscode.window.showInformationMessage(`AppMap index process listening on port ${portStr}`);
this.rpcPort = parseInt(portStr);
}
}
}
15 changes: 12 additions & 3 deletions src/services/nodeDependencyProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as childProcess from 'child_process';
import * as vscode from 'vscode';
import * as path from 'path';
import { readFile } from 'fs/promises';

import ExtensionSettings from '../configuration/extensionSettings';

export enum ProgramName {
Expand All @@ -27,6 +28,9 @@ export type SpawnOptions = {

// Indicates whether or not log messages should be retained in a buffer
saveOutput?: boolean;

// If specified, this function will be called with each line of stdout
stdoutListener?: (data: string) => void;
} & Exclude<childProcess.SpawnOptionsWithoutStdio, 'argv0'>;

export type ProgramOptions = {
Expand Down Expand Up @@ -59,7 +63,8 @@ export class ProcessLog extends Array<ProcessLogItem> {
static appendLogger(
process: childProcess.ChildProcessWithoutNullStreams,
outputChannel?: vscode.OutputChannel,
save?: boolean
save?: boolean,
dataListener?: (data: string) => void
): ChildProcess {
if ((process as ChildProcess).log) {
throw new Error(`process ${process.pid} already has a logger appended`);
Expand All @@ -69,7 +74,10 @@ export class ProcessLog extends Array<ProcessLogItem> {

process.stdout.setEncoding('utf8');
process.stderr.setEncoding('utf8');
process.stdout.on('data', (data) => log.append(data, OutputStream.Stdout, true));
process.stdout.on('data', (data) => {
if (dataListener) dataListener(data);
log.append(data, OutputStream.Stdout, true);
});
process.stderr.on('data', (data) => log.append(data, OutputStream.Stderr, true));
process.once('error', (err) =>
log.append(`an error occurred: ${String(err)}`, OutputStream.Stderr)
Expand Down Expand Up @@ -147,7 +155,8 @@ export function spawn(options: SpawnOptions): ChildProcess {
const loggedProcess = ProcessLog.appendLogger(
newProcess as childProcess.ChildProcessWithoutNullStreams,
options.log,
options.saveOutput
options.saveOutput,
options.stdoutListener
);

return loggedProcess;
Expand Down
Loading
Loading