diff --git a/extensions/ql-vscode/gulpfile.js/webpack.config.ts b/extensions/ql-vscode/gulpfile.js/webpack.config.ts index e0a1a386f..7a1e09d66 100644 --- a/extensions/ql-vscode/gulpfile.js/webpack.config.ts +++ b/extensions/ql-vscode/gulpfile.js/webpack.config.ts @@ -5,7 +5,7 @@ export const config: webpack.Configuration = { mode: 'development', entry: { resultsView: './src/view/results.tsx', - compareView: './src/compare/view/compare.tsx', + compareView: './src/compare/view/Compare.tsx', }, output: { path: path.resolve(__dirname, '..', 'out'), diff --git a/extensions/ql-vscode/src/compare/compare-interface.ts b/extensions/ql-vscode/src/compare/compare-interface.ts index 0ceadc0e0..c40ea728f 100644 --- a/extensions/ql-vscode/src/compare/compare-interface.ts +++ b/extensions/ql-vscode/src/compare/compare-interface.ts @@ -1,15 +1,30 @@ import { DisposableObject } from "semmle-vscode-utils"; -import { WebviewPanel, ExtensionContext, window as Window, ViewColumn, Uri } from "vscode"; -import * as path from 'path'; +import { + WebviewPanel, + ExtensionContext, + window as Window, + ViewColumn, + Uri, +} from "vscode"; +import * as path from "path"; import { tmpDir } from "../run-queries"; import { CompletedQuery } from "../query-results"; -import { CompareViewMessage } from "../interface-types"; +import { + FromCompareViewMessage, + ToCompareViewMessage, + QueryCompareResult, +} from "../interface-types"; import { Logger } from "../logging"; import { CodeQLCliServer } from "../cli"; import { DatabaseManager } from "../databases"; -import { getHtmlForWebview, WebviewReveal } from "../webview-utils"; -import { showAndLogErrorMessage } from "../helpers"; +import { + getHtmlForWebview, + jumpToLocation, +} from "../webview-utils"; +import { adaptSchema, adaptBqrs, RawResultSet } from "../adapt"; +import { BQRSInfo } from "../bqrs-cli-types"; +import resultsDiff from "./resultsDiff"; interface ComparePair { from: CompletedQuery; @@ -19,6 +34,8 @@ interface ComparePair { export class CompareInterfaceManager extends DisposableObject { private comparePair: ComparePair | undefined; private panel: WebviewPanel | undefined; + private panelLoaded = false; + private panelLoadedCallBacks: (() => void)[] = []; constructor( public ctx: ExtensionContext, @@ -29,10 +46,49 @@ export class CompareInterfaceManager extends DisposableObject { super(); } - showResults(from: CompletedQuery, to: CompletedQuery, forceReveal = WebviewReveal.NotForced) { + async showResults( + from: CompletedQuery, + to: CompletedQuery, + selectedResultSetName?: string + ) { this.comparePair = { from, to }; - if (forceReveal === WebviewReveal.Forced) { - this.getPanel().reveal(undefined, true); + this.getPanel().reveal(undefined, true); + + await this.waitForPanelLoaded(); + const [ + commonResultSetNames, + currentResultSetName, + fromResultSet, + toResultSet, + ] = await this.findCommonResultSetNames(from, to, selectedResultSetName); + if (currentResultSetName) { + await this.postMessage({ + t: "setComparisons", + stats: { + fromQuery: { + // since we split the description into several rows + // only run interpolation if the label is user-defined + // otherwise we will wind up with duplicated rows + name: from.options.label + ? from.interpolate(from.getLabel()) + : from.queryName, + status: from.statusString, + time: from.time, + }, + toQuery: { + name: to.options.label + ? to.interpolate(from.getLabel()) + : to.queryName, + status: to.statusString, + time: to.time, + }, + }, + columns: fromResultSet.schema.columns, + commonResultSetNames, + currentResultSetName: currentResultSetName, + rows: this.compareResults(fromResultSet, toResultSet), + datebaseUri: to.database.databaseUri, + }); } } @@ -40,9 +96,9 @@ export class CompareInterfaceManager extends DisposableObject { if (this.panel == undefined) { const { ctx } = this; const panel = (this.panel = Window.createWebviewPanel( - "compareView", // internal name - "Compare CodeQL Query Results", // user-visible name - { viewColumn: ViewColumn.Beside, preserveFocus: true }, + "compareView", + "Compare CodeQL Query Results", + { viewColumn: ViewColumn.Active, preserveFocus: true }, { enableScripts: true, enableFindWidget: true, @@ -54,7 +110,10 @@ export class CompareInterfaceManager extends DisposableObject { } )); this.panel.onDidDispose( - () => this.panel = undefined, + () => { + this.panel = undefined; + this.comparePair = undefined; + }, null, ctx.subscriptions ); @@ -64,10 +123,14 @@ export class CompareInterfaceManager extends DisposableObject { ); const stylesheetPathOnDisk = Uri.file( - ctx.asAbsolutePath("out/compareView.css") + ctx.asAbsolutePath("out/resultsView.css") ); - panel.webview.html = getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk); + panel.webview.html = getHtmlForWebview( + panel.webview, + scriptPathOnDisk, + stylesheetPathOnDisk + ); panel.webview.onDidReceiveMessage( async (e) => this.handleMsgFromView(e), undefined, @@ -77,9 +140,103 @@ export class CompareInterfaceManager extends DisposableObject { return this.panel; } - private async handleMsgFromView(msg: CompareViewMessage): Promise { - /** TODO */ - showAndLogErrorMessage(JSON.stringify(msg)); - showAndLogErrorMessage(JSON.stringify(this.comparePair)); + private waitForPanelLoaded(): Promise { + return new Promise((resolve) => { + if (this.panelLoaded) { + resolve(); + } else { + this.panelLoadedCallBacks.push(resolve); + } + }); + } + + private async handleMsgFromView(msg: FromCompareViewMessage): Promise { + switch (msg.t) { + case "compareViewLoaded": + this.panelLoaded = true; + this.panelLoadedCallBacks.forEach((cb) => cb()); + this.panelLoadedCallBacks = []; + break; + + case "changeCompare": + this.changeTable(msg.newResultSetName); + break; + + case "viewSourceFile": + await jumpToLocation(msg, this.databaseManager, this.logger); + break; + } + } + + private postMessage(msg: ToCompareViewMessage): Thenable { + return this.getPanel().webview.postMessage(msg); + } + + private async findCommonResultSetNames( + from: CompletedQuery, + to: CompletedQuery, + selectedResultSetName: string | undefined + ): Promise<[string[], string, RawResultSet, RawResultSet]> { + const fromSchemas = await this.cliServer.bqrsInfo( + from.query.resultsPaths.resultsPath + ); + const toSchemas = await this.cliServer.bqrsInfo( + to.query.resultsPaths.resultsPath + ); + const fromSchemaNames = fromSchemas["result-sets"].map( + (schema) => schema.name + ); + const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name); + const commonResultSetNames = fromSchemaNames.filter((name) => + toSchemaNames.includes(name) + ); + const currentResultSetName = selectedResultSetName || commonResultSetNames[0]; + const fromResultSet = await this.getResultSet( + fromSchemas, + currentResultSetName, + from.query.resultsPaths.resultsPath + ); + const toResultSet = await this.getResultSet( + toSchemas, + currentResultSetName, + to.query.resultsPaths.resultsPath + ); + return [ + commonResultSetNames, + currentResultSetName, + fromResultSet, + toResultSet, + ]; + } + + private async changeTable(newResultSetName: string) { + if (!this.comparePair?.from || !this.comparePair.to) { + return; + } + await this.showResults(this.comparePair.from, this.comparePair.to, newResultSetName); + } + + private async getResultSet( + bqrsInfo: BQRSInfo, + resultSetName: string, + resultsPath: string + ): Promise { + const schema = bqrsInfo["result-sets"].find( + (schema) => schema.name === resultSetName + ); + if (!schema) { + throw new Error(`Schema ${resultSetName} not found.`); + } + const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName); + const adaptedSchema = adaptSchema(schema); + return adaptBqrs(adaptedSchema, chunk); + } + + private compareResults( + fromResults: RawResultSet, + toResults: RawResultSet + ): QueryCompareResult { + // Only compare columns that have the same name + return resultsDiff(fromResults, toResults); } } diff --git a/extensions/ql-vscode/src/compare/resultsDiff.ts b/extensions/ql-vscode/src/compare/resultsDiff.ts new file mode 100644 index 000000000..0b80f51d2 --- /dev/null +++ b/extensions/ql-vscode/src/compare/resultsDiff.ts @@ -0,0 +1,58 @@ +import { RawResultSet } from "../adapt"; +import { QueryCompareResult } from "../interface-types"; + +/** + * Compare the rows of two queries. Use deep equality to determine if + * rows have been added or removed across two invocations of a query. + * + * Assumptions: + * + * 1. Queries have the same sort order + * 2. Queries have same number and order of columns + * 3. Rows are not changed or re-ordered, they are only added or removed + * + * @param fromResults the source query + * @param toResults the target query + * + * @throws Error when: + * 1. number of columns do not match + * 2. If either query is empty + * 3. If the queries are 100% disjoint + */ +export default function resultsDiff( + fromResults: RawResultSet, + toResults: RawResultSet +): QueryCompareResult { + + if (fromResults.schema.columns.length !== toResults.schema.columns.length) { + throw new Error("CodeQL Compare: Columns do not match."); + } + + if (!fromResults.rows.length) { + throw new Error("CodeQL Compare: Source query has no results."); + } + + if (!toResults.rows.length) { + throw new Error("CodeQL Compare: Target query has no results."); + } + + const results = { + from: arrayDiff(fromResults.rows, toResults.rows), + to: arrayDiff(toResults.rows, fromResults.rows), + }; + + if ( + fromResults.rows.length === results.from.length && + toResults.rows.length === results.to.length + ) { + throw new Error("CodeQL Compare: No overlap between the selected queries."); + } + + return results; +} + +function arrayDiff(source: readonly T[], toRemove: readonly T[]): T[] { + // Stringify the object so that we can compare hashes in the set + const rest = new Set(toRemove.map((item) => JSON.stringify(item))); + return source.filter((element) => !rest.has(JSON.stringify(element))); +} diff --git a/extensions/ql-vscode/src/compare/view/Compare.tsx b/extensions/ql-vscode/src/compare/view/Compare.tsx new file mode 100644 index 000000000..0e13421e5 --- /dev/null +++ b/extensions/ql-vscode/src/compare/view/Compare.tsx @@ -0,0 +1,128 @@ +import * as React from "react"; +import { useState, useEffect } from "react"; +import * as Rdom from "react-dom"; + +import RawTableHeader from "../../view/RawTableHeader"; +import { + ToCompareViewMessage, + SetComparisonsMessage, +} from "../../interface-types"; +import CompareSelector from "./CompareSelector"; +import { vscode } from "../../view/vscode-api"; +import RawTableRow from "../../view/RawTableRow"; +import { ResultRow } from "../../adapt"; +import { className } from "../../view/result-table-utils"; + +const emptyComparison: SetComparisonsMessage = { + t: "setComparisons", + stats: {}, + rows: { + from: [], + to: [], + }, + columns: [], + commonResultSetNames: [], + currentResultSetName: "", + datebaseUri: "", +}; + +export function Compare(props: {}): JSX.Element { + const [comparison, setComparison] = useState( + emptyComparison + ); + + useEffect(() => { + window.addEventListener("message", (evt: MessageEvent) => { + const msg: ToCompareViewMessage = evt.data; + switch (msg.t) { + case "setComparisons": + setComparison(msg); + } + }); + }); + if (!comparison) { + return
Waiting for results to load.
; + } + + try { + return ( + <> +
+
Table to compare:
+ + vscode.postMessage({ t: "changeCompare", newResultSetName }) + } + /> +
+ + + + + + + + + + + + + + + + + + + + + +
{comparison.stats.fromQuery?.name}{comparison.stats.toQuery?.name}
{comparison.stats.fromQuery?.time}{comparison.stats.toQuery?.time}
{comparison.rows.from.length} rows removed{comparison.rows.to.length} rows added
+ + + {createRows(comparison.rows.from, comparison.datebaseUri)} +
+
+ + + {createRows(comparison.rows.to, comparison.datebaseUri)} +
+
+ + ); + } catch (err) { + console.error(err); + return
Error!
; + } +} + +function createRows(rows: ResultRow[], databaseUri: string) { + return ( + + {rows.map((row, rowIndex) => ( + + ))} + + ); +} + +Rdom.render( + , + document.getElementById("root"), + // Post a message to the extension when fully loaded. + () => vscode.postMessage({ t: "compareViewLoaded" }) +); diff --git a/extensions/ql-vscode/src/compare/view/CompareSelector.tsx b/extensions/ql-vscode/src/compare/view/CompareSelector.tsx new file mode 100644 index 000000000..627def9b3 --- /dev/null +++ b/extensions/ql-vscode/src/compare/view/CompareSelector.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +interface Props { + availableResultSets: string[]; + currentResultSetName: string; + updateResultSet: (newResultSet: string) => void; +} + +export default function CompareSelector(props: Props) { + return ( + + ); +} diff --git a/extensions/ql-vscode/src/compare/view/compare.tsx b/extensions/ql-vscode/src/compare/view/compare.tsx deleted file mode 100644 index b15baa125..000000000 --- a/extensions/ql-vscode/src/compare/view/compare.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import * as Rdom from 'react-dom'; - -interface Props { - /**/ -} - -export function App(props: Props): JSX.Element { - return ( -
Compare View!
- ); -} - -Rdom.render( - , - document.getElementById('root') -); diff --git a/extensions/ql-vscode/src/compare/view/compareView.css b/extensions/ql-vscode/src/compare/view/compareView.css deleted file mode 100644 index b799e4b45..000000000 --- a/extensions/ql-vscode/src/compare/view/compareView.css +++ /dev/null @@ -1,8 +0,0 @@ -.octicon { - fill: var(--vscode-editor-foreground); - margin-top: .25em; -} - -.octicon-light { - opacity: 0.6; -} diff --git a/extensions/ql-vscode/src/compare/view/tsconfig.json b/extensions/ql-vscode/src/compare/view/tsconfig.json index 2af7d89e1..3cbe05b53 100644 --- a/extensions/ql-vscode/src/compare/view/tsconfig.json +++ b/extensions/ql-vscode/src/compare/view/tsconfig.json @@ -10,14 +10,15 @@ ], "jsx": "react", "sourceMap": true, - "rootDir": "..", + "rootDir": "../..", "strict": true, "noUnusedLocals": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "typeRoots" : ["./typings"] }, "exclude": [ "node_modules" ] -} \ No newline at end of file +} diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 04b69be0d..544a90f1f 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -306,7 +306,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu ctx, queryHistoryConfigurationListener, async item => showResultsForCompletedQuery(item, WebviewReveal.Forced), - async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to, WebviewReveal.Forced), + async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to), ); logger.log('Initializing results panel interface.'); const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger); @@ -319,8 +319,12 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu logger.log('Initializing source archive filesystem provider.'); archiveFilesystemProvider.activate(ctx); - async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery, forceReveal: WebviewReveal): Promise { - await cmpm.showResults(from, to, forceReveal); + async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery): Promise { + try { + await cmpm.showResults(from, to); + } catch (e) { + helpers.showAndLogErrorMessage(e.message); + } } async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise { diff --git a/extensions/ql-vscode/src/interface-types.ts b/extensions/ql-vscode/src/interface-types.ts index 7b2d7d514..f09595261 100644 --- a/extensions/ql-vscode/src/interface-types.ts +++ b/extensions/ql-vscode/src/interface-types.ts @@ -1,6 +1,6 @@ import * as sarif from 'sarif'; -import { ResolvableLocationValue } from 'semmle-bqrs'; -import { ParsedResultSets } from './adapt'; +import { ResolvableLocationValue, ColumnSchema } from 'semmle-bqrs'; +import { ResultRow, ParsedResultSets } from './adapt'; /** * Only ever show this many results per run in interpreted results. @@ -110,7 +110,7 @@ export type FromResultsViewMsg = | ResultViewLoaded | ChangePage; -interface ViewSourceFileMsg { +export interface ViewSourceFileMsg { t: 'viewSourceFile'; loc: ResolvableLocationValue; databaseUri: string; @@ -171,8 +171,60 @@ interface ChangeInterpretedResultsSortMsg { sortState?: InterpretedResultsSortState; } -export interface CompareViewMessage { - t: 'change-compare'; +export type FromCompareViewMessage = + | CompareViewLoadedMessage + | ChangeCompareMessage + | ViewSourceFileMsg; + +interface CompareViewLoadedMessage { + t: 'compareViewLoaded'; +} + +interface ChangeCompareMessage { + t: 'changeCompare'; newResultSetName: string; // TODO do we need to include the ids of the queries } + +export type ToCompareViewMessage = SetComparisonsMessage; + +export interface SetComparisonsMessage { + readonly t: "setComparisons"; + readonly stats: { + fromQuery?: { + name: string; + status: string; + time: string; + }; + toQuery?: { + name: string; + status: string; + time: string; + }; + }; + readonly columns: readonly ColumnSchema[]; + readonly commonResultSetNames: string[]; + readonly currentResultSetName: string; + readonly rows: QueryCompareResult; + readonly datebaseUri: string; +} + +export enum DiffKind { + Add = 'Add', + Remove = 'Remove', + Change = 'Change' +} + +/** + * from is the set of rows that have changes in the "from" query. + * to is the set of rows that have changes in the "to" query. + * They are in the same order, so element 1 in "from" corresponds to + * element 1 in "to". + * + * If an array element is null, that means that the element was removed + * (or added) in the comparison. + */ +export type QueryCompareResult = { + from: ResultRow[]; + to: ResultRow[]; +}; diff --git a/extensions/ql-vscode/src/interface-utils.ts b/extensions/ql-vscode/src/interface-utils.ts index 6c045ee2f..2ac744a63 100644 --- a/extensions/ql-vscode/src/interface-utils.ts +++ b/extensions/ql-vscode/src/interface-utils.ts @@ -2,21 +2,31 @@ import { RawResultSet } from './adapt'; import { ResultSetSchema } from 'semmle-bqrs'; import { Interpretation } from './interface-types'; -export const SELECT_TABLE_NAME = '#select'; -export const ALERTS_TABLE_NAME = 'alerts'; +export const SELECT_TABLE_NAME = "#select"; +export const ALERTS_TABLE_NAME = "alerts"; -export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet; -export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation; +export type RawTableResultSet = { t: "RawResultSet" } & RawResultSet; +export type PathTableResultSet = { + t: "SarifResultSet"; + readonly schema: ResultSetSchema; + name: string; +} & Interpretation; -export type ResultSet = - | RawTableResultSet - | PathTableResultSet; +export type ResultSet = RawTableResultSet | PathTableResultSet; export function getDefaultResultSet(resultSets: readonly ResultSet[]): string { - return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name)); + return getDefaultResultSetName( + resultSets.map((resultSet) => resultSet.schema.name) + ); } -export function getDefaultResultSetName(resultSetNames: readonly string[]): string { +export function getDefaultResultSetName( + resultSetNames: readonly string[] +): string { // Choose first available result set from the array - return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0]; + return [ + ALERTS_TABLE_NAME, + SELECT_TABLE_NAME, + resultSetNames[0], + ].filter((resultSetName) => resultSetNames.includes(resultSetName))[0]; } diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts index 784ed79fb..eb7a63d17 100644 --- a/extensions/ql-vscode/src/interface.ts +++ b/extensions/ql-vscode/src/interface.ts @@ -48,7 +48,7 @@ import { getHtmlForWebview, shownLocationDecoration, shownLocationLineDecoration, - showLocation, + jumpToLocation, } from "./webview-utils"; import { getDefaultResultSetName } from "./interface-utils"; @@ -158,7 +158,10 @@ export class InterfaceManager extends DisposableObject { } )); this._panel.onDidDispose( - () => (this._panel = undefined), + () => { + this._panel = undefined; + this._displayedQuery = undefined; + }, null, ctx.subscriptions ); @@ -199,27 +202,8 @@ export class InterfaceManager extends DisposableObject { private async handleMsgFromView(msg: FromResultsViewMsg): Promise { switch (msg.t) { - case 'viewSourceFile': { - const databaseItem = this.databaseManager.findDatabaseItem( - Uri.parse(msg.databaseUri) - ); - if (databaseItem !== undefined) { - try { - await showLocation(msg.loc, databaseItem); - } catch (e) { - if (e instanceof Error) { - if (e.message.match(/File not found/)) { - vscode.window.showErrorMessage( - 'Original file of this result is not in the database\'s source archive.' - ); - } else { - this.logger.log(`Unable to handleMsgFromView: ${e.message}`); - } - } else { - this.logger.log(`Unable to handleMsgFromView: ${e}`); - } - } - } + case "viewSourceFile": { + await jumpToLocation(msg, this.databaseManager, this.logger); break; } case 'toggleDiagnostics': { diff --git a/extensions/ql-vscode/src/languageSupport.ts b/extensions/ql-vscode/src/languageSupport.ts index beffd7e4e..051f1b858 100644 --- a/extensions/ql-vscode/src/languageSupport.ts +++ b/extensions/ql-vscode/src/languageSupport.ts @@ -1,4 +1,4 @@ -import { IndentAction, languages } from 'vscode'; +import { languages } from "vscode"; /** @@ -27,29 +27,29 @@ export function install() { languages.setLanguageConfiguration('dbscheme', langConfig); } -const onEnterRules = [ - { - // e.g. /** | */ - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - afterText: /^\s*\*\/$/, - action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' } - }, { - // e.g. /** ...| - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - action: { indentAction: IndentAction.None, appendText: ' * ' } - }, { - // e.g. * ...| - beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, - oneLineAboveText: /^(\s*(\/\*\*|\*)).*/, - action: { indentAction: IndentAction.None, appendText: '* ' } - }, { - // e.g. */| - beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, - action: { indentAction: IndentAction.None, removeText: 1 } - }, - { - // e.g. *-----*/| - beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, - action: { indentAction: IndentAction.None, removeText: 1 } - } +const onEnterRules: string[] = [ + // { + // // e.g. /** | */ + // beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + // afterText: /^\s*\*\/$/, + // action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' } + // }, { + // // e.g. /** ...| + // beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + // action: { indentAction: IndentAction.None, appendText: ' * ' } + // }, { + // // e.g. * ...| + // beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, + // oneLineAboveText: /^(\s*(\/\*\*|\*)).*/, + // action: { indentAction: IndentAction.None, appendText: '* ' } + // }, { + // // e.g. */| + // beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, + // action: { indentAction: IndentAction.None, removeText: 1 } + // }, + // { + // // e.g. *-----*/| + // beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, + // action: { indentAction: IndentAction.None, removeText: 1 } + // } ]; diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts index 9a542e1b1..d98aa8e6e 100644 --- a/extensions/ql-vscode/src/query-history.ts +++ b/extensions/ql-vscode/src/query-history.ts @@ -145,6 +145,10 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider refresh() { this._onDidChangeTreeData.fire(undefined); } + + find(queryId: number): CompletedQuery | undefined { + return this.allHistory.find(query => query.query.queryID === queryId); + } } /** @@ -346,6 +350,10 @@ export class QueryHistoryManager { return item; } + find(queryId: number): CompletedQuery | undefined { + return this.treeDataProvider.find(queryId); + } + /** * Update the tree view selection if the tree view is visible. * diff --git a/extensions/ql-vscode/src/view/RawTableHeader.tsx b/extensions/ql-vscode/src/view/RawTableHeader.tsx new file mode 100644 index 000000000..b80e09d37 --- /dev/null +++ b/extensions/ql-vscode/src/view/RawTableHeader.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; + +import { vscode } from "./vscode-api"; +import { RawResultsSortState, SortDirection } from "../interface-types"; +import { nextSortDirection } from "./result-table-utils"; +import { ColumnSchema } from "semmle-bqrs"; + +interface Props { + readonly columns: readonly ColumnSchema[]; + readonly schemaName: string; + readonly sortState?: RawResultsSortState; + readonly preventSort?: boolean; +} + +function toggleSortStateForColumn( + index: number, + schemaName: string, + sortState: RawResultsSortState | undefined, + preventSort: boolean +): void { + if (preventSort) { + return; + } + + const prevDirection = + sortState && sortState.columnIndex === index + ? sortState.sortDirection + : undefined; + const nextDirection = nextSortDirection(prevDirection); + const nextSortState = + nextDirection === undefined + ? undefined + : { + columnIndex: index, + sortDirection: nextDirection, + }; + vscode.postMessage({ + t: "changeSort", + resultSetName: schemaName, + sortState: nextSortState, + }); +} + +export default function RawTableHeader(props: Props) { + return ( + + + {[ + ( + + # + + ), + ...props.columns.map((col, index) => { + const displayName = col.name || `[${index}]`; + const sortDirection = + props.sortState && index === props.sortState.columnIndex + ? props.sortState.sortDirection + : undefined; + return ( + + toggleSortStateForColumn( + index, + props.schemaName, + props.sortState, + !!props.preventSort + ) + } + > + {displayName} + + ); + }), + ]} + + + ); +} diff --git a/extensions/ql-vscode/src/view/RawTableRow.tsx b/extensions/ql-vscode/src/view/RawTableRow.tsx new file mode 100644 index 000000000..14acc9a82 --- /dev/null +++ b/extensions/ql-vscode/src/view/RawTableRow.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { ResultRow } from "../adapt"; +import { zebraStripe } from "./result-table-utils"; +import RawTableValue from "./RawTableValue"; + +interface Props { + rowIndex: number; + row: ResultRow; + databaseUri: string; + className?: string; +} + +export default function RawTableRow(props: Props) { + return ( + + {props.rowIndex + 1} + + {props.row.map((value, columnIndex) => ( + + + + ))} + + ); +} diff --git a/extensions/ql-vscode/src/view/RawTableValue.tsx b/extensions/ql-vscode/src/view/RawTableValue.tsx new file mode 100644 index 000000000..aea56cacf --- /dev/null +++ b/extensions/ql-vscode/src/view/RawTableValue.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { ResultValue } from "../adapt"; +import { renderLocation } from "./result-table-utils"; + +interface Props { + value: ResultValue; + databaseUri: string; +} + +export default function RawTableValue(props: Props): JSX.Element { + const v = props.value; + if (typeof v === 'string') { + return {v}; + } + else if ('uri' in v) { + return {v.uri}; + } + else { + return renderLocation(v.location, v.label, props.databaseUri); + } +} diff --git a/extensions/ql-vscode/src/view/alert-table.tsx b/extensions/ql-vscode/src/view/alert-table.tsx index 473a5b257..9ab74e3fe 100644 --- a/extensions/ql-vscode/src/view/alert-table.tsx +++ b/extensions/ql-vscode/src/view/alert-table.tsx @@ -5,10 +5,11 @@ import * as Keys from '../result-keys'; import { LocationStyle } from 'semmle-bqrs'; import * as octicons from './octicons'; import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils'; -import { onNavigation, NavigationEvent, vscode } from './results'; +import { onNavigation, NavigationEvent } from './results'; +import { PathTableResultSet } from '../interface-utils'; import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils'; import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types'; -import { PathTableResultSet } from '../interface-utils'; +import { vscode } from './vscode-api'; export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet }; export interface PathTableState { @@ -188,11 +189,9 @@ export class PathTable extends React.Component { if (result.codeFlows === undefined) { rows.push( - + {octicons.info} - - {msg} - + {msg} {locationCells} ); diff --git a/extensions/ql-vscode/src/view/raw-results-table.tsx b/extensions/ql-vscode/src/view/raw-results-table.tsx index cb6fd0225..a4d119633 100644 --- a/extensions/ql-vscode/src/view/raw-results-table.tsx +++ b/extensions/ql-vscode/src/view/raw-results-table.tsx @@ -1,9 +1,9 @@ -import * as React from 'react'; -import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from './result-table-utils'; -import { vscode } from './results'; -import { ResultValue } from '../adapt'; -import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from '../interface-types'; -import { RawTableResultSet } from '../interface-utils'; +import * as React from "react"; +import { ResultTableProps, className } from "./result-table-utils"; +import { RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types"; +import { RawTableResultSet } from "../interface-utils"; +import RawTableHeader from "./RawTableHeader"; +import RawTableRow from "./RawTableRow"; export type RawTableProps = ResultTableProps & { resultSet: RawTableResultSet; @@ -19,7 +19,7 @@ export class RawTable extends React.Component { render(): React.ReactNode { const { resultSet, databaseUri } = this.props; - let dataRows = this.props.resultSet.rows; + let dataRows = resultSet.rows; let numTruncatedResults = 0; if (dataRows.length > RAW_RESULTS_LIMIT) { numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT; @@ -27,19 +27,12 @@ export class RawTable extends React.Component { } const tableRows = dataRows.map((row, rowIndex) => - - { - [ - {rowIndex + 1 + this.props.offset}, - ...row.map((value, columnIndex) => - - { - renderTupleValue(value, databaseUri) - } - ) - ] - } - + ); if (numTruncatedResults > 0) { @@ -50,53 +43,14 @@ export class RawTable extends React.Component { } return - - - { - [ - , - ...resultSet.schema.columns.map((col, index) => { - const displayName = col.name || `[${index}]`; - const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined; - return ; - }) - ] - } - - + {tableRows}
# this.toggleSortStateForColumn(index)}>{displayName}
; } - - private toggleSortStateForColumn(index: number): void { - const sortState = this.props.sortState; - const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined; - const nextDirection = nextSortDirection(prevDirection); - const nextSortState = nextDirection === undefined ? undefined : { - columnIndex: index, - sortDirection: nextDirection - }; - vscode.postMessage({ - t: 'changeSort', - resultSetName: this.props.resultSet.schema.name, - sortState: nextSortState - }); - } -} - -/** - * Render one column of a tuple. - */ -function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element { - if (typeof v === 'string') { - return {v}; - } - else if ('uri' in v) { - return {v.uri}; - } - else { - return renderLocation(v.location, v.label, databaseUri); - } } diff --git a/extensions/ql-vscode/src/view/result-table-utils.tsx b/extensions/ql-vscode/src/view/result-table-utils.tsx index 0e6e52ce2..de1328f0b 100644 --- a/extensions/ql-vscode/src/view/result-table-utils.tsx +++ b/extensions/ql-vscode/src/view/result-table-utils.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs'; import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types'; -import { vscode } from './results'; import { assertNever } from '../helpers-pure'; import { ResultSet } from '../interface-utils'; +import { vscode } from './vscode-api'; export interface ResultTableProps { resultSet: ResultSet; diff --git a/extensions/ql-vscode/src/view/result-tables.tsx b/extensions/ql-vscode/src/view/result-tables.tsx index 01e5812ce..1f896656c 100644 --- a/extensions/ql-vscode/src/view/result-tables.tsx +++ b/extensions/ql-vscode/src/view/result-tables.tsx @@ -3,9 +3,9 @@ import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, Resul import { PathTable } from './alert-table'; import { RawTable } from './raw-results-table'; import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils'; -import { vscode } from './results'; import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt'; import { ResultSet, ALERTS_TABLE_NAME, SELECT_TABLE_NAME, getDefaultResultSet } from '../interface-utils'; +import { vscode } from './vscode-api'; /** * Properties for the `ResultTables` component. diff --git a/extensions/ql-vscode/src/view/results.tsx b/extensions/ql-vscode/src/view/results.tsx index 92f5f0c76..d69b53267 100644 --- a/extensions/ql-vscode/src/view/results.tsx +++ b/extensions/ql-vscode/src/view/results.tsx @@ -1,13 +1,32 @@ -import * as React from 'react'; -import * as Rdom from 'react-dom'; -import * as bqrs from 'semmle-bqrs'; -import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, tryGetResolvableLocation } from 'semmle-bqrs'; -import { assertNever } from '../helpers-pure'; -import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types'; -import { EventHandlers as EventHandlerList } from './event-handler-list'; -import { ResultTables } from './result-tables'; -import { ResultValue, ResultRow, ParsedResultSets } from '../adapt'; -import { ResultSet } from '../interface-utils'; +import * as React from "react"; +import * as Rdom from "react-dom"; +import * as bqrs from "semmle-bqrs"; +import { + ElementBase, + PrimitiveColumnValue, + PrimitiveTypeKind, + tryGetResolvableLocation, +} from "semmle-bqrs"; +import { assertNever } from "../helpers-pure"; +import { + DatabaseInfo, + Interpretation, + IntoResultsViewMsg, + SortedResultSetInfo, + RawResultsSortState, + NavigatePathMsg, + QueryMetadata, + ResultsPaths, +} from "../interface-types"; +import { EventHandlers as EventHandlerList } from "./event-handler-list"; +import { ResultTables } from "./result-tables"; +import { + ResultValue, + ResultRow, + ParsedResultSets, +} from "../adapt"; +import { ResultSet } from "../interface-utils"; +import { vscode } from "./vscode-api"; /** * results.tsx @@ -16,18 +35,13 @@ import { ResultSet } from '../interface-utils'; * Displaying query results. */ -interface VsCodeApi { - /** - * Post message back to vscode extension. - */ - postMessage(msg: FromResultsViewMsg): void; -} -declare const acquireVsCodeApi: () => VsCodeApi; -export const vscode = acquireVsCodeApi(); - -async function* getChunkIterator(response: Response): AsyncIterableIterator { +async function* getChunkIterator( + response: Response +): AsyncIterableIterator { if (!response.ok) { - throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`); + throw new Error( + `Failed to load results: (${response.status}) ${response.statusText}` + ); } const reader = response.body!.getReader(); while (true) { @@ -39,23 +53,28 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator { +async function parseResultSets( + response: Response +): Promise { const chunks = getChunkIterator(response); const resultSets: ResultSet[] = []; @@ -64,32 +83,33 @@ async function parseResultSets(response: Response): Promise column.type); const rows: ResultRow[] = []; resultSets.push({ - t: 'RawResultSet', + t: "RawResultSet", schema: resultSetSchema, - rows: rows + rows: rows, }); return (tuple) => { const row: ResultValue[] = []; tuple.forEach((value, index) => { const type = columnTypes[index]; - if (type.type === 'e') { + if (type.type === "e") { const element: ElementBase = value as ElementBase; - const label = (element.label !== undefined) ? element.label : element.id.toString(); //REVIEW: URLs? + const label = + element.label !== undefined ? element.label : element.id.toString(); //REVIEW: URLs? const resolvableLocation = tryGetResolvableLocation(element.location); if (resolvableLocation !== undefined) { row.push({ label: label, - location: resolvableLocation + location: resolvableLocation, }); - } - else { + } else { // No location link. row.push(label); } - } - else { - row.push(translatePrimitiveValue(value as PrimitiveColumnValue, type.type)); + } else { + row.push( + translatePrimitiveValue(value as PrimitiveColumnValue, type.type) + ); } }); @@ -151,16 +171,16 @@ class App extends React.Component<{}, ResultsViewState> { displayedResults: { resultsInfo: null, results: null, - errorMessage: '' + errorMessage: "", }, nextResultsInfo: null, - isExpectingResultsUpdate: true + isExpectingResultsUpdate: true, }; } handleMessage(msg: IntoResultsViewMsg): void { switch (msg.t) { - case 'setState': + case "setState": this.updateStateWithNewResultsInfo({ resultsPath: msg.resultsPath, parsedResultSets: msg.parsedResultSets, @@ -168,18 +188,19 @@ class App extends React.Component<{}, ResultsViewState> { sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)), database: msg.database, interpretation: msg.interpretation, - shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering, - metadata: msg.metadata + shouldKeepOldResultsWhileRendering: + msg.shouldKeepOldResultsWhileRendering, + metadata: msg.metadata, }); this.loadResults(); break; - case 'resultsUpdating': + case "resultsUpdating": this.setState({ - isExpectingResultsUpdate: true + isExpectingResultsUpdate: true, }); break; - case 'navigatePath': + case "navigatePath": onNavigation.fire(msg); break; default: @@ -188,11 +209,13 @@ class App extends React.Component<{}, ResultsViewState> { } private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void { - this.setState(prevState => { - const stateWithDisplayedResults = (displayedResults: ResultsState): ResultsViewState => ({ + this.setState((prevState) => { + const stateWithDisplayedResults = ( + displayedResults: ResultsState + ): ResultsViewState => ({ displayedResults, isExpectingResultsUpdate: prevState.isExpectingResultsUpdate, - nextResultsInfo: resultsInfo + nextResultsInfo: resultsInfo, }); if (!prevState.isExpectingResultsUpdate && resultsInfo === null) { @@ -200,7 +223,7 @@ class App extends React.Component<{}, ResultsViewState> { return stateWithDisplayedResults({ resultsInfo: null, results: null, - errorMessage: 'No results to display' + errorMessage: "No results to display", }); } if (!resultsInfo || !resultsInfo.shouldKeepOldResultsWhileRendering) { @@ -208,19 +231,22 @@ class App extends React.Component<{}, ResultsViewState> { return stateWithDisplayedResults({ resultsInfo: null, results: null, - errorMessage: 'Loading results…' + errorMessage: "Loading results…", }); } return stateWithDisplayedResults(prevState.displayedResults); }); } - private async getResultSets(resultsInfo: ResultsInfo): Promise { + private async getResultSets( + resultsInfo: ResultsInfo + ): Promise { const parsedResultSets = resultsInfo.parsedResultSets; switch (parsedResultSets.t) { - case 'WebviewParsed': return await this.fetchResultSets(resultsInfo); - case 'ExtensionParsed': { - return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }]; + case "WebviewParsed": + return await this.fetchResultSets(resultsInfo); + case "ExtensionParsed": { + return [{ t: "RawResultSet", ...parsedResultSets.resultSet }]; } } } @@ -232,27 +258,26 @@ class App extends React.Component<{}, ResultsViewState> { } let results: Results | null = null; - let statusText = ''; + let statusText = ""; try { const resultSets = await this.getResultSets(resultsInfo); results = { resultSets, database: resultsInfo.database, - sortStates: this.getSortStates(resultsInfo) + sortStates: this.getSortStates(resultsInfo), }; - } - catch (e) { + } catch (e) { let errorMessage: string; if (e instanceof Error) { errorMessage = e.message; } else { - errorMessage = 'Unknown error'; + errorMessage = "Unknown error"; } statusText = `Error loading results: ${errorMessage}`; } - this.setState(prevState => { + this.setState((prevState) => { // Only set state if this results info is still current. if (resultsInfo !== prevState.nextResultsInfo) { return null; @@ -261,10 +286,10 @@ class App extends React.Component<{}, ResultsViewState> { displayedResults: { resultsInfo, results, - errorMessage: statusText + errorMessage: statusText, }, nextResultsInfo: null, - isExpectingResultsUpdate: false + isExpectingResultsUpdate: false, }; }); } @@ -273,67 +298,99 @@ class App extends React.Component<{}, ResultsViewState> { * This is deprecated, because it calls `fetch`. We are moving * towards doing all bqrs parsing in the extension. */ - private async fetchResultSets(resultsInfo: ResultsInfo): Promise { + private async fetchResultSets( + resultsInfo: ResultsInfo + ): Promise { const unsortedResponse = await fetch(resultsInfo.resultsPath); const unsortedResultSets = await parseResultSets(unsortedResponse); - return Promise.all(unsortedResultSets.map(async unsortedResultSet => { - const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(unsortedResultSet.schema.name); - if (sortedResultSetInfo === undefined) { - return unsortedResultSet; - } - const response = await fetch(sortedResultSetInfo.resultsPath); - const resultSets = await parseResultSets(response); - if (resultSets.length != 1) { - throw new Error(`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`); - } - return resultSets[0]; - })); + return Promise.all( + unsortedResultSets.map(async (unsortedResultSet) => { + const sortedResultSetInfo = resultsInfo.sortedResultsMap.get( + unsortedResultSet.schema.name + ); + if (sortedResultSetInfo === undefined) { + return unsortedResultSet; + } + const response = await fetch(sortedResultSetInfo.resultsPath); + const resultSets = await parseResultSets(response); + if (resultSets.length != 1) { + throw new Error( + `Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.` + ); + } + return resultSets[0]; + }) + ); } - private getSortStates(resultsInfo: ResultsInfo): Map { + private getSortStates( + resultsInfo: ResultsInfo + ): Map { const entries = Array.from(resultsInfo.sortedResultsMap.entries()); - return new Map(entries.map(([key, sortedResultSetInfo]) => - [key, sortedResultSetInfo.sortState])); + return new Map( + entries.map(([key, sortedResultSetInfo]) => [ + key, + sortedResultSetInfo.sortState, + ]) + ); } render(): JSX.Element { const displayedResults = this.state.displayedResults; - if (displayedResults.results !== null && displayedResults.resultsInfo !== null) { + if ( + displayedResults.results !== null && + displayedResults.resultsInfo !== null + ) { const parsedResultSets = displayedResults.resultsInfo.parsedResultSets; - return ; - } - else { + return ( + + ); + } else { return {displayedResults.errorMessage}; } } componentDidMount(): void { - this.vscodeMessageHandler = evt => this.handleMessage(evt.data as IntoResultsViewMsg); - window.addEventListener('message', this.vscodeMessageHandler); + this.vscodeMessageHandler = (evt) => + this.handleMessage(evt.data as IntoResultsViewMsg); + window.addEventListener("message", this.vscodeMessageHandler); } componentWillUnmount(): void { if (this.vscodeMessageHandler) { - window.removeEventListener('message', this.vscodeMessageHandler); + window.removeEventListener("message", this.vscodeMessageHandler); } } - private vscodeMessageHandler: ((ev: MessageEvent) => void) | undefined = undefined; + private vscodeMessageHandler: + | ((ev: MessageEvent) => void) + | undefined = undefined; } -Rdom.render( - , - document.getElementById('root') -); +Rdom.render(, document.getElementById("root")); vscode.postMessage({ t: 'resultViewLoaded' }); diff --git a/extensions/ql-vscode/src/view/resultsView.css b/extensions/ql-vscode/src/view/resultsView.css index 59fbc9857..52a858c70 100644 --- a/extensions/ql-vscode/src/view/resultsView.css +++ b/extensions/ql-vscode/src/view/resultsView.css @@ -19,7 +19,7 @@ margin-left: auto; } -.vscode-codeql__result-table-toggle-diagnostics { +.vscode-codeql__result-table-toggle-diagnostics { display: inline-block; } @@ -29,16 +29,17 @@ display: inline-block; vertical-align: middle; } + .vscode-codeql__result-table-toggle-diagnostics input { margin: 3px 3px 1px 13px; } .vscode-codeql__result-table th { - border-top: 1px solid rgba(88,96,105,0.25); - border-bottom: 1px solid rgba(88,96,105,0.25); - font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; - background: rgba(225,228,232, 0.25); + border-top: 1px solid rgba(88, 96, 105, 0.25); + border-bottom: 1px solid rgba(88, 96, 105, 0.25); + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; + background: rgba(225, 228, 232, 0.25); padding: 0.25em 0.5em; text-align: center; font-weight: normal; @@ -48,8 +49,8 @@ .vscode-codeql__result-table .sort-asc, .vscode-codeql__result-table .sort-desc, .vscode-codeql__result-table .sort-none { - cursor: pointer; - user-select: none; + cursor: pointer; + user-select: none; } .vscode-codeql__result-table .sort-none::after { @@ -155,3 +156,12 @@ td.vscode-codeql__path-index-cell { .number-of-results { padding-left: 3em; } + +.vscode-codeql__compare-header { + display: flex; +} + +.vscode-codeql__compare-body { + margin: 20px 0; + width: 100%; +} diff --git a/extensions/ql-vscode/src/view/vscode-api.ts b/extensions/ql-vscode/src/view/vscode-api.ts new file mode 100644 index 000000000..9aa55c8a9 --- /dev/null +++ b/extensions/ql-vscode/src/view/vscode-api.ts @@ -0,0 +1,11 @@ +import { FromCompareViewMessage, FromResultsViewMsg } from "../interface-types"; + +export interface VsCodeApi { + /** + * Post message back to vscode extension. + */ + postMessage(msg: FromResultsViewMsg | FromCompareViewMessage): void; +} + +declare const acquireVsCodeApi: () => VsCodeApi; +export const vscode = acquireVsCodeApi(); diff --git a/extensions/ql-vscode/src/webview-utils.ts b/extensions/ql-vscode/src/webview-utils.ts index 3c05cc9f6..c03c1f7d4 100644 --- a/extensions/ql-vscode/src/webview-utils.ts +++ b/extensions/ql-vscode/src/webview-utils.ts @@ -20,7 +20,9 @@ import { WholeFileLocation, ResolvableLocationValue, } from "semmle-bqrs"; -import { DatabaseItem } from "./databases"; +import { DatabaseItem, DatabaseManager } from "./databases"; +import { ViewSourceFileMsg } from "./interface-types"; +import { Logger } from "./logging"; /** Gets a nonce string created with 128 bits of entropy. */ export function getNonce(): string { @@ -197,3 +199,30 @@ export const shownLocationLineDecoration = Window.createTextEditorDecorationType isWholeLine: true, } ); + +export async function jumpToLocation( + msg: ViewSourceFileMsg, + databaseManager: DatabaseManager, + logger: Logger +) { + const databaseItem = databaseManager.findDatabaseItem( + Uri.parse(msg.databaseUri) + ); + if (databaseItem !== undefined) { + try { + await showLocation(msg.loc, databaseItem); + } catch (e) { + if (e instanceof Error) { + if (e.message.match(/File not found/)) { + Window.showErrorMessage( + `Original file of this result is not in the database's source archive.` + ); + } else { + logger.log(`Unable to handleMsgFromView: ${e.message}`); + } + } else { + logger.log(`Unable to handleMsgFromView: ${e}`); + } + } + } +}