Extract shared components out of interface.ts
This is in preparation for creating a new webview, extract shared functionality to the webview-utils file
This commit is contained in:
Родитель
e38a34edce
Коммит
2ab4c1ac14
|
@ -21,7 +21,8 @@ import {
|
|||
import * as helpers from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager, WebviewReveal } from './interface';
|
||||
import { InterfaceManager } from './interface';
|
||||
import { WebviewReveal } from './webview-utils';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedQuery } from './query-results';
|
||||
|
@ -30,6 +31,7 @@ import { displayQuickQuery } from './quick-query';
|
|||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
|
@ -303,14 +305,24 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
|||
const qhm = new QueryHistoryManager(
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
|
||||
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced),
|
||||
async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to, WebviewReveal.Forced),
|
||||
);
|
||||
logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
ctx.subscriptions.push(intm);
|
||||
|
||||
logger.log('Initializing compare panel interface.');
|
||||
const cmpm = new CompareInterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
ctx.subscriptions.push(cmpm);
|
||||
|
||||
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 showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,56 @@
|
|||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import * as Sarif from 'sarif';
|
||||
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import * as vscode from 'vscode';
|
||||
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace, env } from 'vscode';
|
||||
import * as cli from './cli';
|
||||
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection, RAW_RESULTS_PAGE_SIZE } from './interface-types';
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
|
||||
import { adaptSchema, adaptBqrs, RawResultSet, ParsedResultSets } from './adapt';
|
||||
import { EXPERIMENTAL_BQRS_SETTING } from './config';
|
||||
import { getDefaultResultSetName } from './interface-utils';
|
||||
import * as path from "path";
|
||||
import * as Sarif from "sarif";
|
||||
import { DisposableObject } from "@github/codeql-vscode-utils";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
Diagnostic,
|
||||
DiagnosticRelatedInformation,
|
||||
DiagnosticSeverity,
|
||||
languages,
|
||||
Uri,
|
||||
window as Window,
|
||||
env
|
||||
} from "vscode";
|
||||
import * as cli from "./cli";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { DatabaseItem, DatabaseManager } from "./databases";
|
||||
import { showAndLogErrorMessage } from "./helpers";
|
||||
import { assertNever } from "./helpers-pure";
|
||||
import {
|
||||
FromResultsViewMsg,
|
||||
Interpretation,
|
||||
INTERPRETED_RESULTS_PER_RUN_LIMIT,
|
||||
IntoResultsViewMsg,
|
||||
QueryMetadata,
|
||||
ResultsPaths,
|
||||
SortedResultSetInfo,
|
||||
SortedResultsMap,
|
||||
InterpretedResultsSortState,
|
||||
SortDirection,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
} from "./interface-types";
|
||||
import { Logger } from "./logging";
|
||||
import * as messages from "./messages";
|
||||
import { CompletedQuery, interpretResults } from "./query-results";
|
||||
import { QueryInfo, tmpDir } from "./run-queries";
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from "./sarif-utils";
|
||||
import {
|
||||
adaptSchema,
|
||||
adaptBqrs,
|
||||
ParsedResultSets,
|
||||
RawResultSet,
|
||||
} from "./adapt";
|
||||
import { EXPERIMENTAL_BQRS_SETTING } from "./config";
|
||||
import {
|
||||
WebviewReveal,
|
||||
fileUriToWebviewUri,
|
||||
tryResolveLocation,
|
||||
getHtmlForWebview,
|
||||
shownLocationDecoration,
|
||||
shownLocationLineDecoration,
|
||||
showLocation,
|
||||
} from "./webview-utils";
|
||||
import { getDefaultResultSetName } from "./interface-utils";
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
|
@ -29,87 +60,30 @@ import { getDefaultResultSetName } from './interface-utils';
|
|||
* webview asks us to.
|
||||
*/
|
||||
|
||||
/** Gets a nonce string created with 128 bits of entropy. */
|
||||
function getNonce(): string {
|
||||
return crypto.randomBytes(16).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to force webview to reveal
|
||||
*/
|
||||
export enum WebviewReveal {
|
||||
Forced,
|
||||
NotForced,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML to populate the given webview.
|
||||
* Uses a content security policy that only loads the given script.
|
||||
*/
|
||||
function getHtmlForWebview(
|
||||
webview: vscode.Webview,
|
||||
scriptUriOnDisk: vscode.Uri,
|
||||
stylesheetUriOnDisk: vscode.Uri
|
||||
): void {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
/*
|
||||
* Content security policy:
|
||||
* default-src: allow nothing by default.
|
||||
* script-src: allow only the given script, using the nonce.
|
||||
* style-src: allow only the given stylesheet, using the nonce.
|
||||
* connect-src: only allow fetch calls to webview resource URIs
|
||||
* (this is used to load BQRS result files).
|
||||
*/
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div id=root>
|
||||
</div>
|
||||
<script nonce="${nonce}" src="${scriptWebviewUri}">
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
webview.html = html;
|
||||
}
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
export function webviewUriToFileUri(webviewUri: string): Uri {
|
||||
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
|
||||
const path = Uri.parse(webviewUri).path;
|
||||
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
|
||||
return Uri.file(path);
|
||||
}
|
||||
|
||||
function sortMultiplier(sortDirection: SortDirection): number {
|
||||
switch (sortDirection) {
|
||||
case SortDirection.asc: return 1;
|
||||
case SortDirection.desc: return -1;
|
||||
case SortDirection.asc:
|
||||
return 1;
|
||||
case SortDirection.desc:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
|
||||
function sortInterpretedResults(
|
||||
results: Sarif.Result[],
|
||||
sortState: InterpretedResultsSortState | undefined
|
||||
): void {
|
||||
if (sortState !== undefined) {
|
||||
const multiplier = sortMultiplier(sortState.sortDirection);
|
||||
switch (sortState.sortBy) {
|
||||
case 'alert-message':
|
||||
case "alert-message":
|
||||
results.sort((a, b) =>
|
||||
a.message.text === undefined ? 0 :
|
||||
b.message.text === undefined ? 0 :
|
||||
multiplier * (a.message.text?.localeCompare(b.message.text, env.language)));
|
||||
a.message.text === undefined
|
||||
? 0
|
||||
: b.message.text === undefined
|
||||
? 0
|
||||
: multiplier * a.message.text?.localeCompare(b.message.text, env.language)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(sortState.sortBy);
|
||||
|
@ -145,7 +119,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
this.handleSelectionChange.bind(this)
|
||||
)
|
||||
);
|
||||
logger.log('Registering path-step navigation commands.');
|
||||
logger.log("Registering path-step navigation commands.");
|
||||
this.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryResults.nextPathStep',
|
||||
|
@ -184,9 +158,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
}
|
||||
));
|
||||
this._panel.onDidDispose(
|
||||
() => {
|
||||
this._panel = undefined;
|
||||
},
|
||||
() => (this._panel = undefined),
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
@ -196,13 +168,13 @@ export class InterfaceManager extends DisposableObject {
|
|||
const stylesheetPathOnDisk = vscode.Uri.file(
|
||||
ctx.asAbsolutePath('out/resultsView.css')
|
||||
);
|
||||
getHtmlForWebview(
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
stylesheetPathOnDisk
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async e => this.handleMsgFromView(e),
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
@ -222,16 +194,10 @@ export class InterfaceManager extends DisposableObject {
|
|||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await update(this._displayedQuery);
|
||||
await this.showResults(
|
||||
this._displayedQuery,
|
||||
WebviewReveal.NotForced,
|
||||
true
|
||||
);
|
||||
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
|
||||
}
|
||||
|
||||
private async handleMsgFromView(
|
||||
msg: FromResultsViewMsg
|
||||
): Promise<void> {
|
||||
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'viewSourceFile': {
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(
|
||||
|
@ -247,9 +213,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
'Original file of this result is not in the database\'s source archive.'
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Unable to handleMsgFromView: ${e.message}`
|
||||
);
|
||||
this.logger.log(`Unable to handleMsgFromView: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
|
@ -278,7 +242,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
}
|
||||
case 'resultViewLoaded':
|
||||
this._panelLoaded = true;
|
||||
this._panelLoadedCallBacks.forEach(cb => cb());
|
||||
this._panelLoadedCallBacks.forEach((cb) => cb());
|
||||
this._panelLoadedCallBacks = [];
|
||||
break;
|
||||
case 'changeSort':
|
||||
|
@ -308,7 +272,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
}
|
||||
|
||||
private waitForPanelLoaded(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
if (this._panelLoaded) {
|
||||
resolve();
|
||||
} else {
|
||||
|
@ -343,9 +307,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
|
||||
v
|
||||
))
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
this._displayedQuery = results;
|
||||
|
@ -363,12 +325,14 @@ export class InterfaceManager extends DisposableObject {
|
|||
const showButton = 'View Results';
|
||||
const queryName = results.queryName;
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''}.`,
|
||||
`Finished running query ${
|
||||
queryName.length > 0 ? ` "${queryName}"` : ""
|
||||
}.`,
|
||||
showButton
|
||||
);
|
||||
// Address this click asynchronously so we still update the
|
||||
// query history immediately.
|
||||
resultPromise.then(result => {
|
||||
resultPromise.then((result) => {
|
||||
if (result === showButton) {
|
||||
panel.reveal();
|
||||
}
|
||||
|
@ -377,33 +341,44 @@ export class InterfaceManager extends DisposableObject {
|
|||
|
||||
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
|
||||
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
RAW_RESULTS_PAGE_SIZE
|
||||
);
|
||||
|
||||
const resultSetNames = schemas['result-sets'].map(resultSet => resultSet.name);
|
||||
const resultSetNames = schemas["result-sets"].map(
|
||||
(resultSet) => resultSet.name
|
||||
);
|
||||
|
||||
// This may not wind up being the page we actually show, if there are interpreted results,
|
||||
// but speculatively send it anyway.
|
||||
const selectedTable = getDefaultResultSetName(resultSetNames);
|
||||
const schema = schemas['result-sets'].find(resultSet => resultSet.name == selectedTable)!;
|
||||
const schema = schemas["result-sets"].find(
|
||||
(resultSet) => resultSet.name == selectedTable
|
||||
)!;
|
||||
if (schema === undefined) {
|
||||
return { t: 'WebviewParsed' };
|
||||
return { t: "WebviewParsed" };
|
||||
}
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[0]);
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
schema.name,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
schema.pagination?.offsets[0]
|
||||
);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
|
||||
return {
|
||||
t: 'ExtensionParsed',
|
||||
t: "ExtensionParsed",
|
||||
pageNumber: 0,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
resultSet,
|
||||
selectedTable: undefined,
|
||||
resultSetNames
|
||||
resultSetNames,
|
||||
};
|
||||
}
|
||||
else {
|
||||
return { t: 'WebviewParsed' };
|
||||
} else {
|
||||
return { t: "WebviewParsed" };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -418,46 +393,59 @@ export class InterfaceManager extends DisposableObject {
|
|||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
metadata: results.query.metadata
|
||||
metadata: results.query.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a page of raw results from the chosen table.
|
||||
*/
|
||||
public async showPageOfResults(selectedTable: string, pageNumber: number): Promise<void> {
|
||||
public async showPageOfResults(
|
||||
selectedTable: string,
|
||||
pageNumber: number
|
||||
): Promise<void> {
|
||||
const results = this._displayedQuery;
|
||||
if (results === undefined) {
|
||||
throw new Error('trying to view a page of a query that is not loaded');
|
||||
throw new Error("trying to view a page of a query that is not loaded");
|
||||
}
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
|
||||
v
|
||||
))
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
RAW_RESULTS_PAGE_SIZE
|
||||
);
|
||||
|
||||
const resultSetNames = schemas['result-sets'].map(resultSet => resultSet.name);
|
||||
const resultSetNames = schemas["result-sets"].map(
|
||||
(resultSet) => resultSet.name
|
||||
);
|
||||
|
||||
const schema = schemas['result-sets'].find(resultSet => resultSet.name == selectedTable)!;
|
||||
const schema = schemas["result-sets"].find(
|
||||
(resultSet) => resultSet.name == selectedTable
|
||||
)!;
|
||||
if (schema === undefined)
|
||||
throw new Error(`Query result set '${selectedTable}' not found.`);
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[pageNumber]);
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
schema.name,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
schema.pagination?.offsets[pageNumber]
|
||||
);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
|
||||
const parsedResultSets: ParsedResultSets = {
|
||||
t: 'ExtensionParsed',
|
||||
t: "ExtensionParsed",
|
||||
pageNumber,
|
||||
resultSet,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
selectedTable: selectedTable,
|
||||
resultSetNames
|
||||
resultSetNames,
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
|
@ -471,7 +459,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.query.metadata
|
||||
metadata: results.query.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -496,16 +484,13 @@ export class InterfaceManager extends DisposableObject {
|
|||
// unresponsive.
|
||||
|
||||
let numTruncatedResults = 0;
|
||||
sarif.runs.forEach(run => {
|
||||
sarif.runs.forEach((run) => {
|
||||
if (run.results !== undefined) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
|
||||
numTruncatedResults +=
|
||||
run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
|
||||
run.results = run.results.slice(
|
||||
0,
|
||||
INTERPRETED_RESULTS_PER_RUN_LIMIT
|
||||
);
|
||||
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -513,7 +498,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
sarif,
|
||||
sourceLocationPrefix,
|
||||
numTruncatedResults,
|
||||
sortState
|
||||
sortState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -536,7 +521,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
sourceLocationPrefix,
|
||||
};
|
||||
interpretation = await this.getTruncatedResults(
|
||||
query.metadata,
|
||||
|
@ -570,7 +555,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
sourceLocationPrefix,
|
||||
};
|
||||
const interpretation = await this.getTruncatedResults(
|
||||
metadata,
|
||||
|
@ -581,10 +566,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
);
|
||||
|
||||
try {
|
||||
await this.showProblemResultsAsDiagnostics(
|
||||
interpretation,
|
||||
database
|
||||
);
|
||||
await this.showProblemResultsAsDiagnostics(interpretation, database);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : e.toString();
|
||||
this.logger.log(
|
||||
|
@ -687,7 +669,7 @@ export class InterfaceManager extends DisposableObject {
|
|||
): SortedResultSetInfo {
|
||||
return {
|
||||
resultsPath: this.convertPathToWebviewUri(info.resultsPath),
|
||||
sortState: info.sortState
|
||||
sortState: info.sortState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -704,93 +686,3 @@ export class InterfaceManager extends DisposableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const findMatchBackground = new vscode.ThemeColor('editor.findMatchBackground');
|
||||
const findRangeHighlightBackground = new vscode.ThemeColor('editor.findRangeHighlightBackground');
|
||||
|
||||
const shownLocationDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: findMatchBackground,
|
||||
});
|
||||
|
||||
const shownLocationLineDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: findRangeHighlightBackground,
|
||||
isWholeLine: true
|
||||
});
|
||||
|
||||
async function showLocation(loc: ResolvableLocationValue, databaseItem: DatabaseItem): Promise<void> {
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
const doc = await workspace.openTextDocument(resolvedLocation.uri);
|
||||
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
|
||||
const editor = editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
const range = resolvedLocation.range;
|
||||
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
|
||||
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
|
||||
// For reference:
|
||||
// - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected.
|
||||
// - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection.
|
||||
// - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not.
|
||||
//
|
||||
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
|
||||
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
|
||||
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
|
||||
const selectionEnd = (range.start.line === range.end.line)
|
||||
? range.end
|
||||
: range.start;
|
||||
editor.selection = new vscode.Selection(range.start, selectionEnd);
|
||||
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
|
||||
editor.setDecorations(shownLocationDecoration, [range]);
|
||||
editor.setDecorations(shownLocationLineDecoration, [range]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function resolveFivePartLocation(loc: FivePartLocation, databaseItem: DatabaseItem): Location {
|
||||
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
|
||||
// are one-based. Adjust accordingly.
|
||||
const range = new Range(Math.max(0, loc.lineStart - 1),
|
||||
Math.max(0, loc.colStart - 1),
|
||||
Math.max(0, loc.lineEnd - 1),
|
||||
Math.max(0, loc.colEnd));
|
||||
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the filesystem resource location.
|
||||
*/
|
||||
function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: DatabaseItem): Location {
|
||||
// A location corresponding to the start of the file.
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function tryResolveLocation(loc: LocationValue | undefined,
|
||||
databaseItem: DatabaseItem): Location | undefined {
|
||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||
if (resolvableLoc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (resolvableLoc.t) {
|
||||
case LocationStyle.FivePart:
|
||||
return resolveFivePartLocation(resolvableLoc, databaseItem);
|
||||
case LocationStyle.WholeFile:
|
||||
return resolveWholeFileLocation(resolvableLoc, databaseItem);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
import * as crypto from "crypto";
|
||||
import { Uri, Location, Range, WebviewPanel, Webview, window as Window, workspace, ViewColumn, Selection, TextEditorRevealType, ThemeColor } from "vscode";
|
||||
import {
|
||||
FivePartLocation,
|
||||
LocationStyle,
|
||||
LocationValue,
|
||||
tryGetResolvableLocation,
|
||||
WholeFileLocation,
|
||||
ResolvableLocationValue,
|
||||
} from "semmle-bqrs";
|
||||
import { DatabaseItem } from "./databases";
|
||||
|
||||
/** Gets a nonce string created with 128 bits of entropy. */
|
||||
export function getNonce(): string {
|
||||
return crypto.randomBytes(16).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to force webview to reveal
|
||||
*/
|
||||
export enum WebviewReveal {
|
||||
Forced,
|
||||
NotForced,
|
||||
}
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(
|
||||
panel: WebviewPanel,
|
||||
fileUriOnDisk: Uri
|
||||
): string {
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
export function webviewUriToFileUri(webviewUri: string): Uri {
|
||||
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
|
||||
const path = Uri.parse(webviewUri).path;
|
||||
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
|
||||
return Uri.file(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
export function resolveFivePartLocation(
|
||||
loc: FivePartLocation,
|
||||
databaseItem: DatabaseItem
|
||||
): Location {
|
||||
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
|
||||
// are one-based. Adjust accordingly.
|
||||
const range = new Range(
|
||||
Math.max(0, loc.lineStart - 1),
|
||||
Math.max(0, loc.colStart - 1),
|
||||
Math.max(0, loc.lineEnd - 1),
|
||||
Math.max(0, loc.colEnd)
|
||||
);
|
||||
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the filesystem resource location.
|
||||
*/
|
||||
export function resolveWholeFileLocation(
|
||||
loc: WholeFileLocation,
|
||||
databaseItem: DatabaseItem
|
||||
): Location {
|
||||
// A location corresponding to the start of the file.
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
export function tryResolveLocation(
|
||||
loc: LocationValue | undefined,
|
||||
databaseItem: DatabaseItem
|
||||
): Location | undefined {
|
||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||
if (resolvableLoc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (resolvableLoc.t) {
|
||||
case LocationStyle.FivePart:
|
||||
return resolveFivePartLocation(resolvableLoc, databaseItem);
|
||||
case LocationStyle.WholeFile:
|
||||
return resolveWholeFileLocation(resolvableLoc, databaseItem);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML to populate the given webview.
|
||||
* Uses a content security policy that only loads the given script.
|
||||
*/
|
||||
export function getHtmlForWebview(
|
||||
webview: Webview,
|
||||
scriptUriOnDisk: Uri,
|
||||
stylesheetUriOnDisk: Uri
|
||||
): string {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
/*
|
||||
* Content security policy:
|
||||
* default-src: allow nothing by default.
|
||||
* script-src: allow only the given script, using the nonce.
|
||||
* style-src: allow only the given stylesheet, using the nonce.
|
||||
* connect-src: only allow fetch calls to webview resource URIs
|
||||
* (this is used to load BQRS result files).
|
||||
*/
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div id=root>
|
||||
</div>
|
||||
<script nonce="${nonce}" src="${scriptWebviewUri}">
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
|
||||
const findMatchBackground = new ThemeColor("editor.findMatchBackground");
|
||||
const findRangeHighlightBackground = new ThemeColor(
|
||||
"editor.findRangeHighlightBackground"
|
||||
);
|
||||
|
||||
export const shownLocationDecoration = Window.createTextEditorDecorationType(
|
||||
{
|
||||
backgroundColor: findMatchBackground,
|
||||
}
|
||||
);
|
||||
|
||||
export const shownLocationLineDecoration = Window.createTextEditorDecorationType(
|
||||
{
|
||||
backgroundColor: findRangeHighlightBackground,
|
||||
isWholeLine: true,
|
||||
}
|
||||
);
|
||||
|
||||
export async function showLocation(
|
||||
loc: ResolvableLocationValue,
|
||||
databaseItem: DatabaseItem
|
||||
): Promise<void> {
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
const doc = await workspace.openTextDocument(resolvedLocation.uri);
|
||||
const editorsWithDoc = Window.visibleTextEditors.filter(
|
||||
(e) => e.document === doc
|
||||
);
|
||||
const editor =
|
||||
editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, ViewColumn.One);
|
||||
const range = resolvedLocation.range;
|
||||
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
|
||||
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
|
||||
// For reference:
|
||||
// - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected.
|
||||
// - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection.
|
||||
// - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not.
|
||||
//
|
||||
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
|
||||
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
|
||||
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
|
||||
const selectionEnd =
|
||||
range.start.line === range.end.line ? range.end : range.start;
|
||||
editor.selection = new Selection(range.start, selectionEnd);
|
||||
editor.revealRange(range, TextEditorRevealType.InCenter);
|
||||
editor.setDecorations(shownLocationDecoration, [range]);
|
||||
editor.setDecorations(shownLocationLineDecoration, [range]);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче