Skip to content

Commit 783a8a8

Browse files
authored
Merge pull request #1290 from github/aeisenberg/remote-history-label-editing
Allow remote query items to have their labels edited
2 parents a1bc7eb + d6d0825 commit 783a8a8

14 files changed

Lines changed: 307 additions & 150 deletions

extensions/ql-vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@
224224
},
225225
"codeQL.queryHistory.format": {
226226
"type": "string",
227-
"default": "%q on %d - %s, %r result count [%t]",
227+
"default": "%q on %d - %s, %r [%t]",
228228
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
229229
},
230230
"codeQL.queryHistory.ttl": {

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 * as path from 'path';
3+
import { QueryHistoryConfig } from './config';
4+
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
5+
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
6+
7+
interface InterpolateReplacements {
8+
t: string; // Start time
9+
q: string; // Query name
10+
d: string; // Database/Controller repo name
11+
r: string; // Result count/Empty
12+
s: string; // Status
13+
f: string; // Query file name
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} results`,
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: path.basename(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: 28 additions & 22 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
import { Credentials } from './authentication';
4041
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
4142

@@ -123,7 +124,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
123124

124125
private current: QueryHistoryInfo | undefined;
125126

126-
constructor(extensionPath: string) {
127+
constructor(
128+
extensionPath: string,
129+
private readonly labelProvider: HistoryItemLabelProvider,
130+
) {
127131
super();
128132
this.failedIconPath = path.join(
129133
extensionPath,
@@ -140,13 +144,13 @@ export class HistoryTreeDataProvider extends DisposableObject {
140144
}
141145

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

145149
treeItem.command = {
146150
title: 'Query History Item',
147151
command: 'codeQLQueryHistory.itemClicked',
148152
arguments: [element],
149-
tooltip: element.failureReason || element.label
153+
tooltip: element.failureReason || this.labelProvider.getLabel(element)
150154
};
151155

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

188-
const h1Label = h1.label.toLowerCase();
189-
const h2Label = h2.label.toLowerCase();
192+
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
193+
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
190194

191195
const h1Date = h1.t === 'local'
192196
? h1.initialInfo.start.getTime()
@@ -315,12 +319,13 @@ export class QueryHistoryManager extends DisposableObject {
315319
._onWillOpenQueryItem.event;
316320

317321
constructor(
318-
private qs: QueryServerClient,
319-
private dbm: DatabaseManager,
320-
private queryStorageDir: string,
321-
private ctx: ExtensionContext,
322-
private queryHistoryConfigListener: QueryHistoryConfig,
323-
private doCompareCallback: (
322+
private readonly qs: QueryServerClient,
323+
private readonly dbm: DatabaseManager,
324+
private readonly queryStorageDir: string,
325+
private readonly ctx: ExtensionContext,
326+
private readonly queryHistoryConfigListener: QueryHistoryConfig,
327+
private readonly labelProvider: HistoryItemLabelProvider,
328+
private readonly doCompareCallback: (
324329
from: CompletedLocalQueryInfo,
325330
to: CompletedLocalQueryInfo
326331
) => Promise<void>
@@ -334,7 +339,8 @@ export class QueryHistoryManager extends DisposableObject {
334339
this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);
335340

336341
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
337-
ctx.extensionPath
342+
ctx.extensionPath,
343+
this.labelProvider
338344
));
339345
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
340346
treeDataProvider: this.treeDataProvider,
@@ -537,7 +543,7 @@ export class QueryHistoryManager extends DisposableObject {
537543

538544
async readQueryHistory(): Promise<void> {
539545
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
540-
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
546+
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
541547
this.treeDataProvider.allHistory = history;
542548
this.treeDataProvider.allHistory.forEach((item) => {
543549
this._onDidAddQueryItem.fire(item);
@@ -605,7 +611,7 @@ export class QueryHistoryManager extends DisposableObject {
605611
// Remote queries can be removed locally, but not remotely.
606612
// The user must cancel the query on GitHub Actions explicitly.
607613
this.treeDataProvider.remove(item);
608-
void logger.log(`Deleted ${item.label}.`);
614+
void logger.log(`Deleted ${this.labelProvider.getLabel(item)}.`);
609615
if (item.status === QueryStatus.InProgress) {
610616
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.');
611617
}
@@ -652,20 +658,20 @@ export class QueryHistoryManager extends DisposableObject {
652658
): Promise<void> {
653659
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
654660

655-
// TODO will support remote queries
656-
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
661+
if (!this.assertSingleQuery(finalMultiSelect)) {
657662
return;
658663
}
659664

660665
const response = await window.showInputBox({
661-
prompt: 'Label:',
662-
placeHolder: '(use default)',
663-
value: finalSingleItem.label,
666+
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
667+
value: finalSingleItem.userSpecifiedLabel ?? '',
668+
title: 'Set query label',
669+
prompt: 'Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.',
664670
});
665671
// undefined response means the user cancelled the dialog; don't change anything
666672
if (response !== undefined) {
667673
// Interpret empty string response as 'go back to using default'
668-
finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
674+
finalSingleItem.userSpecifiedLabel = response === '' ? undefined : response;
669675
await this.refreshTreeView();
670676
}
671677
}
@@ -895,7 +901,7 @@ export class QueryHistoryManager extends DisposableObject {
895901
query.resultsPaths.interpretedResultsPath
896902
);
897903
} else {
898-
const label = finalSingleItem.label;
904+
const label = this.labelProvider.getLabel(finalSingleItem);
899905
void showAndLogInformationMessage(
900906
`Query ${label} has no interpreted results.`
901907
);
@@ -1084,7 +1090,7 @@ the file in the file explorer and dragging it into the workspace.`
10841090
otherQuery.initialInfo.databaseInfo.name === dbName
10851091
)
10861092
.map((item) => ({
1087-
label: item.label,
1093+
label: this.labelProvider.getLabel(item),
10881094
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
10891095
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
10901096
query: item as CompletedLocalQueryInfo,

0 commit comments

Comments
 (0)