Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import {
ProgressReporter,
queryServerLogger,
} from "./common";
import { QueryHistoryManager } from "./query-history/query-history";
import { QueryHistoryManager } from "./query-history/query-history-manager";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
import { QueryServerClient as LegacyQueryServerClient } from "./legacy-query-server/queryserver-client";
import { QueryServerClient } from "./query-server/queryserver-client";
Expand Down Expand Up @@ -1639,7 +1639,7 @@ const checkForUpdatesCommand = "codeQL.checkForUpdatesToCLI";
/**
* This text provider lets us open readonly files in the editor.
*
* TODO: Consolidate this with the 'codeql' text provider in query-history.ts.
* TODO: Consolidate this with the 'codeql' text provider in query-history-manager.ts.
*/
function registerRemoteQueryTextProvider() {
workspace.registerTextDocumentContentProvider("remote-query", {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Diagnostic, DiagnosticSeverity, languages, Range, Uri } from "vscode";
import { DisposableObject } from "../pure/disposable-object";
import { QueryHistoryManager } from "../query-history/query-history";
import { QueryHistoryInfo } from "../query-history/query-history-info";
import {
EvaluationLogProblemReporter,
Expand All @@ -9,6 +8,7 @@ import {
import { PipelineInfo, SummarySymbols } from "./summary-parser";
import { readFile } from "fs-extra";
import { extLogger } from "../common";
import { QueryHistoryManager } from "../query-history/query-history-manager";

/**
* Compute the key used to find a predicate in the summary symbols.
Expand Down
247 changes: 247 additions & 0 deletions extensions/ql-vscode/src/query-history/history-tree-data-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import {
env,
Event,
EventEmitter,
ProviderResult,
ThemeColor,
ThemeIcon,
TreeDataProvider,
TreeItem,
} from "vscode";
import { DisposableObject } from "../pure/disposable-object";
import { assertNever } from "../pure/helpers-pure";
import { QueryHistoryInfo } from "./query-history-info";
import { QueryStatus } from "../query-status";
import { HistoryItemLabelProvider } from "./history-item-label-provider";

export enum SortOrder {
NameAsc = "NameAsc",
NameDesc = "NameDesc",
DateAsc = "DateAsc",
DateDesc = "DateDesc",
CountAsc = "CountAsc",
CountDesc = "CountDesc",
}

/**
* Tree data provider for the query history view.
*/
export class HistoryTreeDataProvider
extends DisposableObject
implements TreeDataProvider<QueryHistoryInfo>
{
private _sortOrder = SortOrder.DateAsc;

private _onDidChangeTreeData = super.push(
new EventEmitter<QueryHistoryInfo | undefined>(),
);

readonly onDidChangeTreeData: Event<QueryHistoryInfo | undefined> =
this._onDidChangeTreeData.event;

private _onDidChangeCurrentQueryItem = super.push(
new EventEmitter<QueryHistoryInfo | undefined>(),
);

public readonly onDidChangeCurrentQueryItem =
this._onDidChangeCurrentQueryItem.event;

private history: QueryHistoryInfo[] = [];

private current: QueryHistoryInfo | undefined;

constructor(private readonly labelProvider: HistoryItemLabelProvider) {
super();
}

async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
const treeItem = new TreeItem(this.labelProvider.getLabel(element));

treeItem.command = {
title: "Query History Item",
command: "codeQLQueryHistory.itemClicked",
arguments: [element],
tooltip: element.failureReason || this.labelProvider.getLabel(element),
};

// Populate the icon and the context value. We use the context value to
// control which commands are visible in the context menu.
treeItem.iconPath = this.getIconPath(element);
treeItem.contextValue = await this.getContextValue(element);

return treeItem;
}

private getIconPath(element: QueryHistoryInfo): ThemeIcon | string {
switch (element.status) {
case QueryStatus.InProgress:
return new ThemeIcon("sync~spin");
case QueryStatus.Completed:
if (element.t === "local") {
return new ThemeIcon("database");
} else {
return new ThemeIcon("cloud");
}
case QueryStatus.Failed:
return new ThemeIcon("error", new ThemeColor("errorForeground"));
default:
assertNever(element.status);
}
}

private async getContextValue(element: QueryHistoryInfo): Promise<string> {
switch (element.status) {
case QueryStatus.InProgress:
if (element.t === "local") {
return "inProgressResultsItem";
} else if (
element.t === "variant-analysis" &&
element.variantAnalysis.actionsWorkflowRunId === undefined
) {
return "pendingRemoteResultsItem";
} else {
return "inProgressRemoteResultsItem";
}
case QueryStatus.Completed:
if (element.t === "local") {
const hasResults =
await element.completedQuery?.query.hasInterpretedResults();
return hasResults ? "interpretedResultsItem" : "rawResultsItem";
} else {
return "remoteResultsItem";
}
case QueryStatus.Failed:
return element.t === "local"
? "cancelledResultsItem"
: "cancelledRemoteResultsItem";
default:
assertNever(element.status);
}
}

getChildren(element?: QueryHistoryInfo): ProviderResult<QueryHistoryInfo[]> {
return element
? []
: this.history.sort((h1, h2) => {
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();

const h1Date = this.getItemDate(h1);

const h2Date = this.getItemDate(h2);

const resultCount1 =
h1.t === "local"
? h1.completedQuery?.resultCount ?? -1
: h1.resultCount ?? -1;
const resultCount2 =
h2.t === "local"
? h2.completedQuery?.resultCount ?? -1
: h2.resultCount ?? -1;

switch (this.sortOrder) {
case SortOrder.NameAsc:
return h1Label.localeCompare(h2Label, env.language);

case SortOrder.NameDesc:
return h2Label.localeCompare(h1Label, env.language);

case SortOrder.DateAsc:
return h1Date - h2Date;

case SortOrder.DateDesc:
return h2Date - h1Date;

case SortOrder.CountAsc:
// If the result counts are equal, sort by name.
return resultCount1 - resultCount2 === 0
? h1Label.localeCompare(h2Label, env.language)
: resultCount1 - resultCount2;

case SortOrder.CountDesc:
// If the result counts are equal, sort by name.
return resultCount2 - resultCount1 === 0
? h2Label.localeCompare(h1Label, env.language)
: resultCount2 - resultCount1;
default:
assertNever(this.sortOrder);
}
});
}

getParent(_element: QueryHistoryInfo): ProviderResult<QueryHistoryInfo> {
return null;
}

getCurrent(): QueryHistoryInfo | undefined {
return this.current;
}

pushQuery(item: QueryHistoryInfo): void {
this.history.push(item);
this.setCurrentItem(item);
this.refresh();
}

setCurrentItem(item?: QueryHistoryInfo) {
if (item !== this.current) {
this.current = item;
this._onDidChangeCurrentQueryItem.fire(item);
}
}

remove(item: QueryHistoryInfo) {
const isCurrent = this.current === item;
if (isCurrent) {
this.setCurrentItem();
}
const index = this.history.findIndex((i) => i === item);
if (index >= 0) {
this.history.splice(index, 1);
if (isCurrent && this.history.length > 0) {
// Try to keep a current item, near the deleted item if there
// are any available.
this.setCurrentItem(
this.history[Math.min(index, this.history.length - 1)],
);
}
this.refresh();
}
}

get allHistory(): QueryHistoryInfo[] {
return this.history;
}

set allHistory(history: QueryHistoryInfo[]) {
this.history = history;
this.setCurrentItem(history[0]);
this.refresh();
}

refresh() {
this._onDidChangeTreeData.fire(undefined);
}

public get sortOrder() {
return this._sortOrder;
}

public set sortOrder(newSortOrder: SortOrder) {
this._sortOrder = newSortOrder;
this._onDidChangeTreeData.fire(undefined);
}

private getItemDate(item: QueryHistoryInfo) {
switch (item.t) {
case "local":
return item.initialInfo.start.getTime();
case "remote":
return item.remoteQuery.executionStartTime;
case "variant-analysis":
return item.variantAnalysis.executionStartTime;
default:
assertNever(item);
}
}
}
Loading