Skip to content

Commit 542e1d2

Browse files
committed
Allow remote query items to have their labels edited
The labels for remote query items are interpolated using the same strategy as local queries with two caveats: 1. There is no easy way to get the result count without reading files, so, this value is kept empty. 2. There is no database name for remote queries. Instead, use the nwo of the controller repo.
1 parent a3deec7 commit 542e1d2

13 files changed

Lines changed: 307 additions & 148 deletions

extensions/ql-vscode/src/compare/compare-interface.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli
2222
import resultsDiff from './resultsDiff';
2323
import { CompletedLocalQueryInfo } from '../query-results';
2424
import { getErrorMessage } from '../pure/helpers-pure';
25+
import { HistoryItemLabelProvider } from '../history-item-label-provider';
2526

2627
interface ComparePair {
2728
from: CompletedLocalQueryInfo;
@@ -39,6 +40,7 @@ export class CompareInterfaceManager extends DisposableObject {
3940
private databaseManager: DatabaseManager,
4041
private cliServer: CodeQLCliServer,
4142
private logger: Logger,
43+
private labelProvider: HistoryItemLabelProvider,
4244
private showQueryResultsCallback: (
4345
item: CompletedLocalQueryInfo
4446
) => Promise<void>
@@ -81,12 +83,12 @@ export class CompareInterfaceManager extends DisposableObject {
8183
// since we split the description into several rows
8284
// only run interpolation if the label is user-defined
8385
// otherwise we will wind up with duplicated rows
84-
name: from.getShortLabel(),
86+
name: this.labelProvider.getShortLabel(from),
8587
status: from.completedQuery.statusString,
8688
time: from.startTime,
8789
},
8890
toQuery: {
89-
name: to.getShortLabel(),
91+
name: this.labelProvider.getShortLabel(to),
9092
status: to.completedQuery.statusString,
9193
time: to.startTime,
9294
},

extensions/ql-vscode/src/extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
9696
import { URLSearchParams } from 'url';
9797
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
9898
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
99+
import { HistoryItemLabelProvider } from './history-item-label-provider';
99100

100101
/**
101102
* extension.ts
@@ -447,6 +448,7 @@ async function activateWithInstalledDistribution(
447448
showResultsForCompletedQuery(item, WebviewReveal.Forced);
448449
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
449450
await fs.ensureDir(queryStorageDir);
451+
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
450452

451453
void logger.log('Initializing query history.');
452454
const qhm = new QueryHistoryManager(
@@ -455,6 +457,7 @@ async function activateWithInstalledDistribution(
455457
queryStorageDir,
456458
ctx,
457459
queryHistoryConfigurationListener,
460+
labelProvider,
458461
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
459462
showResultsForComparison(from, to),
460463
);
@@ -466,8 +469,9 @@ async function activateWithInstalledDistribution(
466469
});
467470

468471
ctx.subscriptions.push(qhm);
472+
469473
void logger.log('Initializing results panel interface.');
470-
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
474+
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
471475
ctx.subscriptions.push(intm);
472476

473477
void logger.log('Initializing compare panel interface.');
@@ -476,6 +480,7 @@ async function activateWithInstalledDistribution(
476480
dbm,
477481
cliServer,
478482
queryServerLogger,
483+
labelProvider,
479484
showResults
480485
);
481486
ctx.subscriptions.push(cmpm);
@@ -525,7 +530,7 @@ async function activateWithInstalledDistribution(
525530
token.onCancellationRequested(() => source.cancel());
526531

527532
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
528-
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
533+
const item = new LocalQueryInfo(initialInfo, source);
529534
qhm.addQuery(item);
530535
try {
531536
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { env } from 'vscode';
2+
import { QueryHistoryConfig } from './config';
3+
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
4+
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
5+
6+
7+
interface InterpolateReplacements {
8+
t: string; // Start time
9+
q: string; // Query name
10+
d: string; // Database/List name
11+
r: string; // Result count
12+
s: string; // Status
13+
f: string; // Query file path
14+
'%': '%'; // Percent sign
15+
}
16+
17+
export class HistoryItemLabelProvider {
18+
constructor(private config: QueryHistoryConfig) {
19+
/**/
20+
}
21+
22+
getLabel(item: QueryHistoryInfo) {
23+
const replacements = item.t === 'local'
24+
? this.getLocalInterpolateReplacements(item)
25+
: this.getRemoteInterpolateReplacements(item);
26+
27+
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
28+
29+
return this.interpolate(rawLabel, replacements);
30+
}
31+
32+
/**
33+
* If there is a user-specified label for this query, interpolate and use that.
34+
* Otherwise, use the raw name of this query.
35+
*
36+
* @returns the name of the query, unless there is a custom label for this query.
37+
*/
38+
getShortLabel(item: QueryHistoryInfo): string {
39+
return item.userSpecifiedLabel
40+
? this.getLabel(item)
41+
: item.t === 'local'
42+
? item.getQueryName()
43+
: item.remoteQuery.queryName;
44+
}
45+
46+
47+
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
48+
return rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
49+
const replacement = replacements[key];
50+
return replacement !== undefined ? replacement : match;
51+
});
52+
}
53+
54+
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
55+
const { resultCount = 0, statusString = 'in progress' } = item.completedQuery || {};
56+
return {
57+
t: item.startTime,
58+
q: item.getQueryName(),
59+
d: item.initialInfo.databaseInfo.name,
60+
r: resultCount.toString(),
61+
s: statusString,
62+
f: item.getQueryFileName(),
63+
'%': '%',
64+
};
65+
}
66+
67+
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
68+
return {
69+
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
70+
q: item.remoteQuery.queryName,
71+
72+
// There is no database name for remote queries. Instead use the controller repository name.
73+
d: `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`,
74+
75+
// There is no synchronous way to get the results count.
76+
r: '',
77+
s: item.status,
78+
f: item.remoteQuery.queryFilePath,
79+
'%': '%'
80+
};
81+
}
82+
}

extensions/ql-vscode/src/interface.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-type
4949
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
5050
import { PAGE_SIZE } from './config';
5151
import { CompletedLocalQueryInfo } from './query-results';
52+
import { HistoryItemLabelProvider } from './history-item-label-provider';
5253

5354
/**
5455
* interface.ts
@@ -136,7 +137,8 @@ export class InterfaceManager extends DisposableObject {
136137
public ctx: vscode.ExtensionContext,
137138
private databaseManager: DatabaseManager,
138139
public cliServer: CodeQLCliServer,
139-
public logger: Logger
140+
public logger: Logger,
141+
private labelProvider: HistoryItemLabelProvider
140142
) {
141143
super();
142144
this.push(this._diagnosticCollection);
@@ -416,7 +418,7 @@ export class InterfaceManager extends DisposableObject {
416418
// more asynchronous message to not so abruptly interrupt
417419
// user's workflow by immediately revealing the panel.
418420
const showButton = 'View Results';
419-
const queryName = fullQuery.getShortLabel();
421+
const queryName = this.labelProvider.getShortLabel(fullQuery);
420422
const resultPromise = vscode.window.showInformationMessage(
421423
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
422424
}.`,
@@ -483,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
483485
database: fullQuery.initialInfo.databaseInfo,
484486
shouldKeepOldResultsWhileRendering,
485487
metadata: fullQuery.completedQuery.query.metadata,
486-
queryName: fullQuery.label,
488+
queryName: this.labelProvider.getLabel(fullQuery),
487489
queryPath: fullQuery.initialInfo.queryPath
488490
});
489491
}
@@ -516,7 +518,7 @@ export class InterfaceManager extends DisposableObject {
516518
resultSetNames,
517519
pageSize: interpretedPageSize(this._interpretation),
518520
numPages: numInterpretedPages(this._interpretation),
519-
queryName: this._displayedQuery.label,
521+
queryName: this.labelProvider.getLabel(this._displayedQuery),
520522
queryPath: this._displayedQuery.initialInfo.queryPath
521523
});
522524
}
@@ -601,7 +603,7 @@ export class InterfaceManager extends DisposableObject {
601603
database: results.initialInfo.databaseInfo,
602604
shouldKeepOldResultsWhileRendering: false,
603605
metadata: results.completedQuery.query.metadata,
604-
queryName: results.label,
606+
queryName: this.labelProvider.getLabel(results),
605607
queryPath: results.initialInfo.queryPath
606608
});
607609
}

extensions/ql-vscode/src/query-history.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { QueryStatus } from './query-status';
3636
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
3737
import * as fs from 'fs-extra';
3838
import { CliVersionConstraint } from './cli';
39+
import { HistoryItemLabelProvider } from './history-item-label-provider';
3940

4041
/**
4142
* query-history.ts
@@ -121,7 +122,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
121122

122123
private current: QueryHistoryInfo | undefined;
123124

124-
constructor(extensionPath: string) {
125+
constructor(
126+
extensionPath: string,
127+
private readonly labelProvider: HistoryItemLabelProvider,
128+
) {
125129
super();
126130
this.failedIconPath = path.join(
127131
extensionPath,
@@ -138,13 +142,13 @@ export class HistoryTreeDataProvider extends DisposableObject {
138142
}
139143

140144
async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
141-
const treeItem = new TreeItem(element.label);
145+
const treeItem = new TreeItem(this.labelProvider.getLabel(element));
142146

143147
treeItem.command = {
144148
title: 'Query History Item',
145149
command: 'codeQLQueryHistory.itemClicked',
146150
arguments: [element],
147-
tooltip: element.failureReason || element.label
151+
tooltip: element.failureReason || this.labelProvider.getLabel(element)
148152
};
149153

150154
// Populate the icon and the context value. We use the context value to
@@ -183,8 +187,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
183187
): ProviderResult<QueryHistoryInfo[]> {
184188
return element ? [] : this.history.sort((h1, h2) => {
185189

186-
const h1Label = h1.label.toLowerCase();
187-
const h2Label = h2.label.toLowerCase();
190+
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
191+
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
188192

189193
const h1Date = h1.t === 'local'
190194
? h1.initialInfo.start.getTime()
@@ -313,12 +317,13 @@ export class QueryHistoryManager extends DisposableObject {
313317
._onWillOpenQueryItem.event;
314318

315319
constructor(
316-
private qs: QueryServerClient,
317-
private dbm: DatabaseManager,
318-
private queryStorageDir: string,
320+
private readonly qs: QueryServerClient,
321+
private readonly dbm: DatabaseManager,
322+
private readonly queryStorageDir: string,
319323
ctx: ExtensionContext,
320-
private queryHistoryConfigListener: QueryHistoryConfig,
321-
private doCompareCallback: (
324+
private readonly queryHistoryConfigListener: QueryHistoryConfig,
325+
private readonly labelProvider: HistoryItemLabelProvider,
326+
private readonly doCompareCallback: (
322327
from: CompletedLocalQueryInfo,
323328
to: CompletedLocalQueryInfo
324329
) => Promise<void>
@@ -332,7 +337,8 @@ export class QueryHistoryManager extends DisposableObject {
332337
this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);
333338

334339
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
335-
ctx.extensionPath
340+
ctx.extensionPath,
341+
this.labelProvider
336342
));
337343
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
338344
treeDataProvider: this.treeDataProvider,
@@ -531,7 +537,7 @@ export class QueryHistoryManager extends DisposableObject {
531537

532538
async readQueryHistory(): Promise<void> {
533539
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
534-
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
540+
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
535541
this.treeDataProvider.allHistory = history;
536542
this.treeDataProvider.allHistory.forEach((item) => {
537543
this._onDidAddQueryItem.fire(item);
@@ -599,7 +605,7 @@ export class QueryHistoryManager extends DisposableObject {
599605
// Remote queries can be removed locally, but not remotely.
600606
// The user must cancel the query on GitHub Actions explicitly.
601607
this.treeDataProvider.remove(item);
602-
void logger.log(`Deleted ${item.label}.`);
608+
void logger.log(`Deleted ${this.labelProvider.getLabel(item)}.`);
603609
if (item.status === QueryStatus.InProgress) {
604610
void logger.log('The variant analysis is still running on GitHub Actions. To cancel there, you must go to the workflow run in your browser.');
605611
}
@@ -647,19 +653,21 @@ export class QueryHistoryManager extends DisposableObject {
647653
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
648654

649655
// TODO will support remote queries
650-
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
656+
if (!this.assertSingleQuery(finalMultiSelect)) {
651657
return;
652658
}
653659

654660
const response = await window.showInputBox({
655-
prompt: 'Label:',
656-
placeHolder: '(use default)',
657-
value: finalSingleItem.label,
661+
// prompt: 'Label:',
662+
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
663+
value: finalSingleItem.userSpecifiedLabel ?? '',
664+
title: 'Set query label',
665+
prompt: 'Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.',
658666
});
659667
// undefined response means the user cancelled the dialog; don't change anything
660668
if (response !== undefined) {
661669
// Interpret empty string response as 'go back to using default'
662-
finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
670+
finalSingleItem.userSpecifiedLabel = response === '' ? undefined : response;
663671
await this.refreshTreeView();
664672
}
665673
}
@@ -816,7 +824,7 @@ export class QueryHistoryManager extends DisposableObject {
816824
}
817825

818826
if (finalSingleItem.evalLogSummaryLocation) {
819-
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
827+
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
820828
} else {
821829
this.warnNoEvalLogSummary();
822830
}
@@ -880,7 +888,7 @@ export class QueryHistoryManager extends DisposableObject {
880888
query.resultsPaths.interpretedResultsPath
881889
);
882890
} else {
883-
const label = finalSingleItem.label;
891+
const label = this.labelProvider.getLabel(finalSingleItem);
884892
void showAndLogInformationMessage(
885893
`Query ${label} has no interpreted results.`
886894
);
@@ -1069,7 +1077,7 @@ the file in the file explorer and dragging it into the workspace.`
10691077
otherQuery.initialInfo.databaseInfo.name === dbName
10701078
)
10711079
.map((item) => ({
1072-
label: item.label,
1080+
label: this.labelProvider.getLabel(item),
10731081
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
10741082
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
10751083
query: item as CompletedLocalQueryInfo,

0 commit comments

Comments
 (0)