This commit is contained in:
Andrew Eisenberg 2020-06-02 16:25:54 -07:00
Родитель e9fbd6d430
Коммит fceea64a08
25 изменённых файлов: 892 добавлений и 297 удалений

Просмотреть файл

@ -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'),

Просмотреть файл

@ -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<void> {
/** TODO */
showAndLogErrorMessage(JSON.stringify(msg));
showAndLogErrorMessage(JSON.stringify(this.comparePair));
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this.panelLoaded) {
resolve();
} else {
this.panelLoadedCallBacks.push(resolve);
}
});
}
private async handleMsgFromView(msg: FromCompareViewMessage): Promise<void> {
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<boolean> {
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<RawResultSet> {
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);
}
}

Просмотреть файл

@ -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<T>(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)));
}

Просмотреть файл

@ -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<SetComparisonsMessage>(
emptyComparison
);
useEffect(() => {
window.addEventListener("message", (evt: MessageEvent) => {
const msg: ToCompareViewMessage = evt.data;
switch (msg.t) {
case "setComparisons":
setComparison(msg);
}
});
});
if (!comparison) {
return <div>Waiting for results to load.</div>;
}
try {
return (
<>
<div className="vscode-codeql__compare-header">
<div>Table to compare:</div>
<CompareSelector
availableResultSets={comparison.commonResultSetNames}
currentResultSetName={comparison.currentResultSetName}
updateResultSet={(newResultSetName: string) =>
vscode.postMessage({ t: "changeCompare", newResultSetName })
}
/>
</div>
<table className="vscode-codeql__compare-body">
<thead>
<tr>
<td>{comparison.stats.fromQuery?.name}</td>
<td>{comparison.stats.toQuery?.name}</td>
</tr>
<tr>
<td>{comparison.stats.fromQuery?.time}</td>
<td>{comparison.stats.toQuery?.time}</td>
</tr>
<tr>
<th>{comparison.rows.from.length} rows removed</th>
<th>{comparison.rows.to.length} rows added</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<table className={className}>
<RawTableHeader
columns={comparison.columns}
schemaName={comparison.currentResultSetName}
preventSort={true}
/>
{createRows(comparison.rows.from, comparison.datebaseUri)}
</table>
</td>
<td>
<table className={className}>
<RawTableHeader
columns={comparison.columns}
schemaName={comparison.currentResultSetName}
preventSort={true}
/>
{createRows(comparison.rows.to, comparison.datebaseUri)}
</table>
</td>
</tr>
</tbody>
</table>
</>
);
} catch (err) {
console.error(err);
return <div>Error!</div>;
}
}
function createRows(rows: ResultRow[], databaseUri: string) {
return (
<tbody>
{rows.map((row, rowIndex) => (
<RawTableRow
key={rowIndex}
rowIndex={rowIndex}
row={row}
databaseUri={databaseUri}
/>
))}
</tbody>
);
}
Rdom.render(
<Compare />,
document.getElementById("root"),
// Post a message to the extension when fully loaded.
() => vscode.postMessage({ t: "compareViewLoaded" })
);

Просмотреть файл

@ -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 (
<select
value={props.currentResultSetName}
onChange={(e) => props.updateResultSet(e.target.value)}
>
{props.availableResultSets.map((resultSet) => (
<option key={resultSet} value={resultSet}>
{resultSet}
</option>
))}
</select>
);
}

Просмотреть файл

@ -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 (
<div>Compare View!</div>
);
}
Rdom.render(
<App />,
document.getElementById('root')
);

Просмотреть файл

@ -1,8 +0,0 @@
.octicon {
fill: var(--vscode-editor-foreground);
margin-top: .25em;
}
.octicon-light {
opacity: 0.6;
}

Просмотреть файл

@ -10,12 +10,13 @@
],
"jsx": "react",
"sourceMap": true,
"rootDir": "..",
"rootDir": "../..",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"typeRoots" : ["./typings"]
},
"exclude": [
"node_modules"

Просмотреть файл

@ -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<void> {
await cmpm.showResults(from, to, forceReveal);
async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery): Promise<void> {
try {
await cmpm.showResults(from, to);
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {

Просмотреть файл

@ -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[];
};

Просмотреть файл

@ -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];
}

Просмотреть файл

@ -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<void> {
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': {

Просмотреть файл

@ -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 }
// }
];

Просмотреть файл

@ -145,6 +145,10 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
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.
*

Просмотреть файл

@ -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 (
<thead>
<tr>
{[
(
<th key={-1}>
<b>#</b>
</th>
),
...props.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection =
props.sortState && index === props.sortState.columnIndex
? props.sortState.sortDirection
: undefined;
return (
<th
className={
"sort-" +
(sortDirection !== undefined
? SortDirection[sortDirection]
: "none")
}
key={index}
onClick={() =>
toggleSortStateForColumn(
index,
props.schemaName,
props.sortState,
!!props.preventSort
)
}
>
<b>{displayName}</b>
</th>
);
}),
]}
</tr>
</thead>
);
}

Просмотреть файл

@ -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 (
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
<td key={-1}>{props.rowIndex + 1}</td>
{props.row.map((value, columnIndex) => (
<td key={columnIndex}>
<RawTableValue
value={value}
databaseUri={props.databaseUri}
/>
</td>
))}
</tr>
);
}

Просмотреть файл

@ -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 <span>{v}</span>;
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;
}
else {
return renderLocation(v.location, v.label, props.databaseUri);
}
}

Просмотреть файл

@ -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<PathTableProps, PathTableState> {
if (result.codeFlows === undefined) {
rows.push(
<tr {...zebraStripe(resultIndex)}>
<tr key={resultIndex} {...zebraStripe(resultIndex)}>
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
<td colSpan={3}>
{msg}
</td>
<td colSpan={3}>{msg}</td>
{locationCells}
</tr>
);

Просмотреть файл

@ -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<RawTableProps, {}> {
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<RawTableProps, {}> {
}
const tableRows = dataRows.map((row, rowIndex) =>
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
{
[
<td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
...row.map((value, columnIndex) =>
<td key={columnIndex}>
{
renderTupleValue(value, databaseUri)
}
</td>)
]
}
</tr>
<RawTableRow
key={rowIndex}
rowIndex={rowIndex}
row={row}
databaseUri={databaseUri}
/>
);
if (numTruncatedResults > 0) {
@ -50,53 +43,14 @@ export class RawTable extends React.Component<RawTableProps, {}> {
}
return <table className={className}>
<thead>
<tr>
{
[
<th key={-1}><b>#</b></th>,
...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 <th className={'sort-' + (sortDirection !== undefined ? SortDirection[sortDirection] : 'none')} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
})
]
}
</tr>
</thead>
<RawTableHeader
columns={resultSet.schema.columns}
schemaName={resultSet.schema.name}
sortState={this.props.sortState}
/>
<tbody>
{tableRows}
</tbody>
</table>;
}
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 <span>{v}</span>;
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;
}
else {
return renderLocation(v.location, v.label, databaseUri);
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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.

Просмотреть файл

@ -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<Uint8Array> {
async function* getChunkIterator(
response: Response
): AsyncIterableIterator<Uint8Array> {
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<Uint
}
}
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind): ResultValue {
function translatePrimitiveValue(
value: PrimitiveColumnValue,
type: PrimitiveTypeKind
): ResultValue {
switch (type) {
case 'i':
case 'f':
case 's':
case 'd':
case 'b':
case "i":
case "f":
case "s":
case "d":
case "b":
return value.toString();
case 'u':
case "u":
return {
uri: value as string
uri: value as string,
};
}
}
async function parseResultSets(response: Response): Promise<readonly ResultSet[]> {
async function parseResultSets(
response: Response
): Promise<readonly ResultSet[]> {
const chunks = getChunkIterator(response);
const resultSets: ResultSet[] = [];
@ -64,32 +83,33 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
const columnTypes = resultSetSchema.columns.map((column) => 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<readonly ResultSet[]> {
private async getResultSets(
resultsInfo: ResultsInfo
): Promise<readonly ResultSet[]> {
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<readonly ResultSet[]> {
private async fetchResultSets(
resultsInfo: ResultsInfo
): Promise<readonly ResultSet[]> {
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<string, RawResultsSortState> {
private getSortStates(
resultsInfo: ResultsInfo
): Map<string, RawResultsSortState> {
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 <ResultTables
parsedResultSets={parsedResultSets}
rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath}
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
sortStates={displayedResults.results.sortStates}
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}
else {
return (
<ResultTables
parsedResultSets={parsedResultSets}
rawResultSets={displayedResults.results.resultSets}
interpretation={
displayedResults.resultsInfo
? displayedResults.resultsInfo.interpretation
: undefined
}
database={displayedResults.results.database}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath}
metadata={
displayedResults.resultsInfo
? displayedResults.resultsInfo.metadata
: undefined
}
sortStates={displayedResults.results.sortStates}
interpretedSortState={
displayedResults.resultsInfo.interpretation?.sortState
}
isLoadingNewResults={
this.state.isExpectingResultsUpdate ||
this.state.nextResultsInfo !== null
}
/>
);
} else {
return <span>{displayedResults.errorMessage}</span>;
}
}
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(
<App />,
document.getElementById('root')
);
Rdom.render(<App />, document.getElementById("root"));
vscode.postMessage({ t: 'resultViewLoaded' });

Просмотреть файл

@ -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%;
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -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}`);
}
}
}
}