diff --git a/extensions/ql-vscode/src/compare/compare-interface.ts b/extensions/ql-vscode/src/compare/compare-interface.ts index 258855e05..7046535db 100644 --- a/extensions/ql-vscode/src/compare/compare-interface.ts +++ b/extensions/ql-vscode/src/compare/compare-interface.ts @@ -35,10 +35,13 @@ export class CompareInterfaceManager extends DisposableObject { private panelLoadedCallBacks: (() => void)[] = []; constructor( - public ctx: ExtensionContext, - public databaseManager: DatabaseManager, - public cliServer: CodeQLCliServer, - public logger: Logger + private ctx: ExtensionContext, + private databaseManager: DatabaseManager, + private cliServer: CodeQLCliServer, + private logger: Logger, + private showQueryResultsCallback: ( + item: CompletedQuery + ) => Promise ) { super(); } @@ -57,7 +60,11 @@ export class CompareInterfaceManager extends DisposableObject { currentResultSetName, fromResultSet, toResultSet, - ] = await this.findCommonResultSetNames(from, to, selectedResultSetName); + ] = await this.findCommonResultSetNames( + from, + to, + selectedResultSetName + ); if (currentResultSetName) { await this.postMessage({ t: "setComparisons", @@ -147,7 +154,9 @@ export class CompareInterfaceManager extends DisposableObject { }); } - private async handleMsgFromView(msg: FromCompareViewMessage): Promise { + private async handleMsgFromView( + msg: FromCompareViewMessage + ): Promise { switch (msg.t) { case "compareViewLoaded": this.panelLoaded = true; @@ -162,6 +171,10 @@ export class CompareInterfaceManager extends DisposableObject { case "viewSourceFile": await jumpToLocation(msg, this.databaseManager, this.logger); break; + + case "openQuery": + await this.openQuery(msg.kind); + break; } } @@ -183,7 +196,9 @@ export class CompareInterfaceManager extends DisposableObject { const fromSchemaNames = fromSchemas["result-sets"].map( (schema) => schema.name ); - const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name); + const toSchemaNames = toSchemas["result-sets"].map( + (schema) => schema.name + ); const commonResultSetNames = fromSchemaNames.filter((name) => toSchemaNames.includes(name) ); @@ -229,7 +244,10 @@ export class CompareInterfaceManager extends DisposableObject { if (!schema) { throw new Error(`Schema ${resultSetName} not found.`); } - const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName); + const chunk = await this.cliServer.bqrsDecode( + resultsPath, + resultSetName + ); const adaptedSchema = adaptSchema(schema); return adaptBqrs(adaptedSchema, chunk); } @@ -241,4 +259,12 @@ export class CompareInterfaceManager extends DisposableObject { // Only compare columns that have the same name return resultsDiff(fromResults, toResults); } + + private openQuery(kind: "from" | "to") { + const toOpen = + kind === "from" ? this.comparePair?.from : this.comparePair?.to; + if (toOpen) { + this.showQueryResultsCallback(toOpen); + } + } } diff --git a/extensions/ql-vscode/src/compare/view/Compare.tsx b/extensions/ql-vscode/src/compare/view/Compare.tsx index 0e13421e5..09039b393 100644 --- a/extensions/ql-vscode/src/compare/view/Compare.tsx +++ b/extensions/ql-vscode/src/compare/view/Compare.tsx @@ -60,8 +60,22 @@ export function Compare(props: {}): JSX.Element { - - + + @@ -105,6 +119,13 @@ export function Compare(props: {}): JSX.Element { } } +async function openQuery(kind: "from" | "to") { + vscode.postMessage({ + t: "openQuery", + kind, + }); +} + function createRows(rows: ResultRow[], databaseUri: string) { return ( diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 9363a2db4..58ef4c4b5 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -272,54 +272,87 @@ export async function activate(ctx: ExtensionContext): Promise { }); } -async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager): Promise { +async function activateWithInstalledDistribution( + ctx: ExtensionContext, + distributionManager: DistributionManager +): Promise { beganMainExtensionActivation = true; // Remove any error stubs command handlers left over from first part // of activation. - errorStubs.forEach(stub => stub.dispose()); + errorStubs.forEach((stub) => stub.dispose()); - logger.log('Initializing configuration listener...'); - const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager); + logger.log("Initializing configuration listener..."); + const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener( + distributionManager + ); ctx.subscriptions.push(qlConfigurationListener); - logger.log('Initializing CodeQL cli server...'); + logger.log("Initializing CodeQL cli server..."); const cliServer = new CodeQLCliServer(distributionManager, logger); ctx.subscriptions.push(cliServer); - logger.log('Initializing query server client.'); - const qs = new qsClient.QueryServerClient(qlConfigurationListener, cliServer, { - logger: queryServerLogger, - }, task => Window.withProgress({ title: 'CodeQL query server', location: ProgressLocation.Window }, task)); + logger.log("Initializing query server client."); + const qs = new qsClient.QueryServerClient( + qlConfigurationListener, + cliServer, + { + logger: queryServerLogger, + }, + (task) => + Window.withProgress( + { title: "CodeQL query server", location: ProgressLocation.Window }, + task + ) + ); ctx.subscriptions.push(qs); await qs.startQueryServer(); - logger.log('Initializing database manager.'); + logger.log("Initializing database manager."); const dbm = new DatabaseManager(ctx, qlConfigurationListener, logger); ctx.subscriptions.push(dbm); - logger.log('Initializing database panel.'); - const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs, getContextStoragePath(ctx)); + logger.log("Initializing database panel."); + const databaseUI = new DatabaseUI( + ctx, + cliServer, + dbm, + qs, + getContextStoragePath(ctx) + ); ctx.subscriptions.push(databaseUI); - logger.log('Initializing query history manager.'); + logger.log("Initializing query history manager."); const queryHistoryConfigurationListener = new QueryHistoryConfigListener(); + const showResults = async (item: CompletedQuery) => + showResultsForCompletedQuery(item, WebviewReveal.Forced); + const qhm = new QueryHistoryManager( ctx, queryHistoryConfigurationListener, - async item => showResultsForCompletedQuery(item, WebviewReveal.Forced), - async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to), + showResults, + async (from: CompletedQuery, to: CompletedQuery) => + showResultsForComparison(from, to), ); - logger.log('Initializing results panel interface.'); + logger.log("Initializing results panel interface."); const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger); ctx.subscriptions.push(intm); - logger.log('Initializing compare panel interface.'); - const cmpm = new CompareInterfaceManager(ctx, dbm, cliServer, queryServerLogger); + logger.log("Initializing compare panel interface."); + const cmpm = new CompareInterfaceManager( + ctx, + dbm, + cliServer, + queryServerLogger, + showResults + ); ctx.subscriptions.push(cmpm); - logger.log('Initializing source archive filesystem provider.'); + logger.log("Initializing source archive filesystem provider."); archiveFilesystemProvider.activate(ctx); - async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery): Promise { + async function showResultsForComparison( + from: CompletedQuery, + to: CompletedQuery + ): Promise { try { await cmpm.showResults(from, to); } catch (e) { @@ -327,18 +360,30 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu } } - async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise { + async function showResultsForCompletedQuery( + query: CompletedQuery, + forceReveal: WebviewReveal + ): Promise { await intm.showResults(query, forceReveal, false); } - async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined): Promise { + async function compileAndRunQuery( + quickEval: boolean, + selectedQuery: Uri | undefined + ): Promise { if (qs !== undefined) { try { const dbItem = await databaseUI.getDatabaseItem(); if (dbItem === undefined) { - throw new Error('Can\'t run query without a selected database'); + throw new Error("Can't run query without a selected database"); } - const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery); + const info = await compileAndRunQueryAgainstDatabase( + cliServer, + qs, + dbItem, + quickEval, + selectedQuery + ); const item = qhm.addQuery(info); await showResultsForCompletedQuery(item, WebviewReveal.NotForced); } catch (e) { @@ -355,21 +400,28 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu ctx.subscriptions.push(tmpDirDisposal); - logger.log('Initializing CodeQL language server.'); - const client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), { - documentSelector: [ - { language: 'ql', scheme: 'file' }, - { language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' } - ], - synchronize: { - configurationSection: 'codeQL' + logger.log("Initializing CodeQL language server."); + const client = new LanguageClient( + "CodeQL Language Server", + () => spawnIdeServer(qlConfigurationListener), + { + documentSelector: [ + { language: "ql", scheme: "file" }, + { language: "yaml", scheme: "file", pattern: "**/qlpack.yml" }, + ], + synchronize: { + configurationSection: "codeQL", + }, + // Ensure that language server exceptions are logged to the same channel as its output. + outputChannel: ideServerLogger.outputChannel, }, - // Ensure that language server exceptions are logged to the same channel as its output. - outputChannel: ideServerLogger.outputChannel - }, true); + true + ); - logger.log('Initializing QLTest interface.'); - const testExplorerExtension = extensions.getExtension(testExplorerExtensionId); + logger.log("Initializing QLTest interface."); + const testExplorerExtension = extensions.getExtension( + testExplorerExtensionId + ); if (testExplorerExtension) { const testHub = testExplorerExtension.exports; const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer); @@ -379,24 +431,58 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu ctx.subscriptions.push(testUIService); } - logger.log('Registering top-level command palette commands.'); - ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri))); - ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri))); - ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI))); - ctx.subscriptions.push(commands.registerCommand('codeQL.restartQueryServer', async () => { - await qs.restartQueryServer(); - helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger }); - })); - ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseFolder', () => databaseUI.handleChooseDatabaseFolder())); - ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseArchive', () => databaseUI.handleChooseDatabaseArchive())); - ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseLgtm', () => databaseUI.handleChooseDatabaseLgtm())); - ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseInternet', () => databaseUI.handleChooseDatabaseInternet())); + logger.log("Registering top-level command palette commands."); + ctx.subscriptions.push( + commands.registerCommand( + "codeQL.runQuery", + async (uri: Uri | undefined) => await compileAndRunQuery(false, uri) + ) + ); + ctx.subscriptions.push( + commands.registerCommand( + "codeQL.quickEval", + async (uri: Uri | undefined) => await compileAndRunQuery(true, uri) + ) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.quickQuery", async () => + displayQuickQuery(ctx, cliServer, databaseUI) + ) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.restartQueryServer", async () => { + await qs.restartQueryServer(); + helpers.showAndLogInformationMessage("CodeQL Query Server restarted.", { + outputLogger: queryServerLogger, + }); + }) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.chooseDatabaseFolder", () => + databaseUI.handleChooseDatabaseFolder() + ) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.chooseDatabaseArchive", () => + databaseUI.handleChooseDatabaseArchive() + ) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.chooseDatabaseLgtm", () => + databaseUI.handleChooseDatabaseLgtm() + ) + ); + ctx.subscriptions.push( + commands.registerCommand("codeQL.chooseDatabaseInternet", () => + databaseUI.handleChooseDatabaseInternet() + ) + ); - logger.log('Starting language server.'); + logger.log("Starting language server."); ctx.subscriptions.push(client.start()); // Jump-to-definition and find-references - logger.log('Registering jump-to-definition handlers.'); + logger.log("Registering jump-to-definition handlers."); languages.registerDefinitionProvider( { scheme: archiveFilesystemProvider.zipArchiveScheme }, new TemplateQueryDefinitionProvider(cliServer, qs, dbm) @@ -406,7 +492,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu new TemplateQueryReferenceProvider(cliServer, qs, dbm) ); - logger.log('Successfully finished extension initialization.'); + logger.log("Successfully finished extension initialization."); } function getContextStoragePath(ctx: ExtensionContext) { diff --git a/extensions/ql-vscode/src/interface-types.ts b/extensions/ql-vscode/src/interface-types.ts index f4a28df66..4a3591a7d 100644 --- a/extensions/ql-vscode/src/interface-types.ts +++ b/extensions/ql-vscode/src/interface-types.ts @@ -1,10 +1,10 @@ -import * as sarif from 'sarif'; +import * as sarif from "sarif"; import { ResolvableLocationValue, ColumnSchema, ResultSetSchema, } from "semmle-bqrs"; -import { ResultRow, ParsedResultSets, RawResultSet } from './adapt'; +import { ResultRow, ParsedResultSets, RawResultSet } from "./adapt"; /** * This module contains types and code that are shared between @@ -87,11 +87,11 @@ export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo }; * As a result of receiving this message, listeners might want to display a loading indicator. */ export interface ResultsUpdatingMsg { - t: 'resultsUpdating'; + t: "resultsUpdating"; } export interface SetStateMsg { - t: 'setState'; + t: "setState"; resultsPath: string; origResultsPaths: ResultsPaths; sortedResultsMap: SortedResultsMap; @@ -115,13 +115,16 @@ export interface SetStateMsg { /** Advance to the next or previous path no in the path viewer */ export interface NavigatePathMsg { - t: 'navigatePath'; + t: "navigatePath"; /** 1 for next, -1 for previous */ direction: number; } -export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg; +export type IntoResultsViewMsg = + | ResultsUpdatingMsg + | SetStateMsg + | NavigatePathMsg; export type FromResultsViewMsg = | ViewSourceFileMsg @@ -132,13 +135,13 @@ export type FromResultsViewMsg = | ChangePage; export interface ViewSourceFileMsg { - t: 'viewSourceFile'; + t: "viewSourceFile"; loc: ResolvableLocationValue; databaseUri: string; } interface ToggleDiagnostics { - t: 'toggleDiagnostics'; + t: "toggleDiagnostics"; databaseUri: string; metadata?: QueryMetadata; origResultsPaths: ResultsPaths; @@ -147,17 +150,18 @@ interface ToggleDiagnostics { } interface ResultViewLoaded { - t: 'resultViewLoaded'; + t: "resultViewLoaded"; } interface ChangePage { - t: 'changePage'; + t: "changePage"; pageNumber: number; // 0-indexed, displayed to the user as 1-indexed selectedTable: string; } export enum SortDirection { - asc, desc + asc, + desc, } export interface RawResultsSortState { @@ -165,8 +169,7 @@ export interface RawResultsSortState { sortDirection: SortDirection; } -export type InterpretedResultsSortColumn = - 'alert-message'; +export type InterpretedResultsSortColumn = "alert-message"; export interface InterpretedResultsSortState { sortBy: InterpretedResultsSortColumn; @@ -174,7 +177,7 @@ export interface InterpretedResultsSortState { } interface ChangeRawResultsSortMsg { - t: 'changeSort'; + t: "changeSort"; resultSetName: string; /** * sortState being undefined means don't sort, just present results in the order @@ -184,7 +187,7 @@ interface ChangeRawResultsSortMsg { } interface ChangeInterpretedResultsSortMsg { - t: 'changeInterpretedSort'; + t: "changeInterpretedSort"; /** * sortState being undefined means don't sort, just present results in the order * they appear in the sarif file. @@ -195,21 +198,26 @@ interface ChangeInterpretedResultsSortMsg { export type FromCompareViewMessage = | CompareViewLoadedMessage | ChangeCompareMessage - | ViewSourceFileMsg; + | ViewSourceFileMsg + | OpenQueryMessage; interface CompareViewLoadedMessage { - t: 'compareViewLoaded'; + t: "compareViewLoaded"; +} + +export interface OpenQueryMessage { + readonly t: "openQuery"; + readonly kind: "from" | "to"; } interface ChangeCompareMessage { - t: 'changeCompare'; + t: "changeCompare"; newResultSetName: string; - // TODO do we need to include the ids of the queries } export type ToCompareViewMessage = SetComparisonsMessage; -export interface SetComparisonsMessage { + export interface SetComparisonsMessage { readonly t: "setComparisons"; readonly stats: { fromQuery?: { @@ -231,9 +239,9 @@ export interface SetComparisonsMessage { } export enum DiffKind { - Add = 'Add', - Remove = 'Remove', - Change = 'Change' + Add = "Add", + Remove = "Remove", + Change = "Change", } /** diff --git a/extensions/ql-vscode/src/view/resultsView.css b/extensions/ql-vscode/src/view/resultsView.css index 52a858c70..23c32d01d 100644 --- a/extensions/ql-vscode/src/view/resultsView.css +++ b/extensions/ql-vscode/src/view/resultsView.css @@ -165,3 +165,8 @@ td.vscode-codeql__path-index-cell { margin: 20px 0; width: 100%; } + +.vscode-codeql__compare-open { + cursor: pointer; + text-decoration: underline; +}
{comparison.stats.fromQuery?.name}{comparison.stats.toQuery?.name} + openQuery("from")} + className="vscode-codeql__compare-open" + > + {comparison.stats.fromQuery?.name} + + + openQuery("to")} + className="vscode-codeql__compare-open" + > + {comparison.stats.toQuery?.name} + +
{comparison.stats.fromQuery?.time}