Skip to content

Commit 3cbaa5a

Browse files
authored
Merge pull request #2935 from github/starcke/language-selection-panel
Add language filter panel.
2 parents a5139b7 + e8e6c6b commit 3cbaa5a

8 files changed

Lines changed: 250 additions & 0 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@
561561
"command": "codeQL.copyVersion",
562562
"title": "CodeQL: Copy Version Information"
563563
},
564+
{
565+
"command": "codeQLLanguageSelection.setSelectedItem",
566+
"title": "Select"
567+
},
564568
{
565569
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
566570
"title": "Run local query",
@@ -1147,6 +1151,11 @@
11471151
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
11481152
"group": "2_qlContextMenu@1"
11491153
},
1154+
{
1155+
"command": "codeQLLanguageSelection.setSelectedItem",
1156+
"when": "view == codeQLLanguageSelection && viewItem =~ /canBeSelected/",
1157+
"group": "inline"
1158+
},
11501159
{
11511160
"command": "codeQLDatabases.setCurrentDatabase",
11521161
"group": "inline",
@@ -1495,6 +1504,10 @@
14951504
{
14961505
"command": "codeQL.openModelEditor"
14971506
},
1507+
{
1508+
"command": "codeQLLanguageSelection.setSelectedItem",
1509+
"when": "false"
1510+
},
14981511
{
14991512
"command": "codeQLQueries.runLocalQueryContextMenu",
15001513
"when": "false"
@@ -1965,6 +1978,11 @@
19651978
},
19661979
"views": {
19671980
"ql-container": [
1981+
{
1982+
"id": "codeQLLanguageSelection",
1983+
"name": "Language",
1984+
"when": "config.codeQL.canary && config.codeQL.showLanguageFilter"
1985+
},
19681986
{
19691987
"id": "codeQLDatabases",
19701988
"name": "Databases"

extensions/ql-vscode/src/common/commands.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from "../variant-analysis/shared/variant-analysis";
1313
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
1414
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
15+
import type { LanguageSelectionTreeViewItem } from "../language-selection-panel/language-selection-data-provider";
1516

1617
// A command function matching the signature that VS Code calls when
1718
// a command is invoked from a context menu on a TreeView with
@@ -198,6 +199,13 @@ export type QueryHistoryCommands = {
198199
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
199200
};
200201

202+
// Commands user for the language selector panel
203+
export type LanguageSelectionCommands = {
204+
"codeQLLanguageSelection.setSelectedItem": (
205+
item: LanguageSelectionTreeViewItem,
206+
) => Promise<void>;
207+
};
208+
201209
// Commands used for the local databases panel
202210
export type LocalDatabasesCommands = {
203211
// Command palette commands
@@ -360,6 +368,7 @@ export type AllExtensionCommands = BaseCommands &
360368
QueryEditorCommands &
361369
ResultsViewCommands &
362370
QueryHistoryCommands &
371+
LanguageSelectionCommands &
363372
LocalDatabasesCommands &
364373
DebuggerCommands &
365374
VariantAnalysisCommands &

extensions/ql-vscode/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server";
136136
import { QueriesModule } from "./queries-panel/queries-module";
137137
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
138138
import { LanguageContextStore } from "./language-context-store";
139+
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
139140

140141
/**
141142
* extension.ts
@@ -779,6 +780,10 @@ async function activateWithInstalledDistribution(
779780
void extLogger.log("Initializing language context.");
780781
const languageContext = new LanguageContextStore(app);
781782

783+
void extLogger.log("Initializing language selector.");
784+
const languageSelectionPanel = new LanguageSelectionPanel(languageContext);
785+
ctx.subscriptions.push(languageSelectionPanel);
786+
782787
void extLogger.log("Initializing database panel.");
783788
const databaseUI = new DatabaseUI(
784789
app,
@@ -1016,6 +1021,7 @@ async function activateWithInstalledDistribution(
10161021
...getPackagingCommands({
10171022
cliServer,
10181023
}),
1024+
...languageSelectionPanel.getCommands(),
10191025
...modelEditorModule.getCommands(),
10201026
...evalLogViewer.getCommands(),
10211027
...summaryLanguageSupport.getCommands(),

extensions/ql-vscode/src/language-context-store.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,28 @@ export class LanguageContextStore extends DisposableObject {
4343
);
4444
}
4545

46+
/**
47+
* This returns true if the given language should be included.
48+
*
49+
* That means that either the given language is selected or the "All" option is selected.
50+
*
51+
* @param language a query language or undefined if the language is unknown.
52+
*/
4653
public shouldInclude(language: QueryLanguage | undefined): boolean {
4754
return this.languageFilter === "All" || this.languageFilter === language;
4855
}
56+
57+
/**
58+
* This returns true if the given language is selected.
59+
*
60+
* If no language is given then it returns true if the "All" option is selected.
61+
*
62+
* @param language a query language or undefined.
63+
*/
64+
public isSelectedLanguage(language: QueryLanguage | undefined): boolean {
65+
return (
66+
(this.languageFilter === "All" && language === undefined) ||
67+
this.languageFilter === language
68+
);
69+
}
4970
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { DisposableObject } from "../common/disposable-object";
2+
import { LanguageContextStore } from "../language-context-store";
3+
import {
4+
Event,
5+
EventEmitter,
6+
ThemeIcon,
7+
TreeDataProvider,
8+
TreeItem,
9+
} from "vscode";
10+
import {
11+
QueryLanguage,
12+
getLanguageDisplayName,
13+
} from "../common/query-language";
14+
15+
const ALL_LANGUAGE_SELECTION_OPTIONS = [
16+
undefined, // All languages
17+
QueryLanguage.Cpp,
18+
QueryLanguage.CSharp,
19+
QueryLanguage.Go,
20+
QueryLanguage.Java,
21+
QueryLanguage.Javascript,
22+
QueryLanguage.Python,
23+
QueryLanguage.Ruby,
24+
QueryLanguage.Swift,
25+
];
26+
27+
// A tree view items consisting of of a language (or undefined for all languages)
28+
// and a boolean indicating whether it is selected or not.
29+
export class LanguageSelectionTreeViewItem extends TreeItem {
30+
constructor(
31+
public readonly language: QueryLanguage | undefined,
32+
public readonly selected: boolean = false,
33+
) {
34+
const label = language ? getLanguageDisplayName(language) : "All languages";
35+
super(label);
36+
37+
this.iconPath = selected ? new ThemeIcon("check") : undefined;
38+
this.contextValue = selected ? undefined : "canBeSelected";
39+
}
40+
}
41+
42+
export class LanguageSelectionTreeDataProvider
43+
extends DisposableObject
44+
implements TreeDataProvider<LanguageSelectionTreeViewItem>
45+
{
46+
private treeItems: LanguageSelectionTreeViewItem[];
47+
private readonly onDidChangeTreeDataEmitter = this.push(
48+
new EventEmitter<void>(),
49+
);
50+
51+
public constructor(private readonly languageContext: LanguageContextStore) {
52+
super();
53+
54+
this.treeItems = this.createTree();
55+
56+
// If the language context changes, we need to update the tree.
57+
this.push(
58+
this.languageContext.onLanguageContextChanged(() => {
59+
this.treeItems = this.createTree();
60+
this.onDidChangeTreeDataEmitter.fire();
61+
}),
62+
);
63+
}
64+
65+
public get onDidChangeTreeData(): Event<void> {
66+
return this.onDidChangeTreeDataEmitter.event;
67+
}
68+
69+
public getTreeItem(item: LanguageSelectionTreeViewItem): TreeItem {
70+
return item;
71+
}
72+
73+
public getChildren(
74+
item?: LanguageSelectionTreeViewItem,
75+
): LanguageSelectionTreeViewItem[] {
76+
if (!item) {
77+
return this.treeItems;
78+
} else {
79+
return [];
80+
}
81+
}
82+
83+
private createTree(): LanguageSelectionTreeViewItem[] {
84+
return ALL_LANGUAGE_SELECTION_OPTIONS.map((language) => {
85+
return new LanguageSelectionTreeViewItem(
86+
language,
87+
this.languageContext.isSelectedLanguage(language),
88+
);
89+
});
90+
}
91+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { DisposableObject } from "../common/disposable-object";
2+
import { window } from "vscode";
3+
import {
4+
LanguageSelectionTreeDataProvider,
5+
LanguageSelectionTreeViewItem,
6+
} from "./language-selection-data-provider";
7+
import { LanguageContextStore } from "../language-context-store";
8+
import { LanguageSelectionCommands } from "../common/commands";
9+
10+
// This panel allows the selection of a single language, that will
11+
// then filter all other relevant views (e.g. db panel, query history).
12+
export class LanguageSelectionPanel extends DisposableObject {
13+
constructor(private readonly languageContext: LanguageContextStore) {
14+
super();
15+
16+
const dataProvider = new LanguageSelectionTreeDataProvider(languageContext);
17+
this.push(dataProvider);
18+
19+
const treeView = window.createTreeView("codeQLLanguageSelection", {
20+
treeDataProvider: dataProvider,
21+
});
22+
this.push(treeView);
23+
}
24+
25+
public getCommands(): LanguageSelectionCommands {
26+
return {
27+
"codeQLLanguageSelection.setSelectedItem":
28+
this.handleSetSelectedLanguage.bind(this),
29+
};
30+
}
31+
32+
private async handleSetSelectedLanguage(
33+
item: LanguageSelectionTreeViewItem,
34+
): Promise<void> {
35+
if (item.language) {
36+
await this.languageContext.setLanguageContext(item.language);
37+
} else {
38+
await this.languageContext.clearLanguageContext();
39+
}
40+
}
41+
}

extensions/ql-vscode/test/unit-tests/command-lint.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe("commands declared in package.json", () => {
3838
expect(title).toBeDefined();
3939
commandTitles[command] = title!;
4040
} else if (
41+
command.match(/^codeQLLanguageSelection\./) ||
4142
command.match(/^codeQLDatabases\./) ||
4243
command.match(/^codeQLQueries\./) ||
4344
command.match(/^codeQLVariantAnalysisRepositories\./) ||
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
QueryLanguage,
3+
getLanguageDisplayName,
4+
} from "../../../../src/common/query-language";
5+
import { LanguageContextStore } from "../../../../src/language-context-store";
6+
import {
7+
LanguageSelectionTreeDataProvider,
8+
LanguageSelectionTreeViewItem,
9+
} from "../../../../src/language-selection-panel/language-selection-data-provider";
10+
import { createMockApp } from "../../../__mocks__/appMock";
11+
import { EventEmitter, ThemeIcon } from "vscode";
12+
13+
describe("LanguageSelectionTreeDataProvider", () => {
14+
function expectSelected(
15+
items: LanguageSelectionTreeViewItem[],
16+
expected: QueryLanguage | undefined,
17+
) {
18+
items.forEach((item) => {
19+
if (item.language === expected) {
20+
expect(item.selected).toBe(true);
21+
expect(item.iconPath).toEqual(new ThemeIcon("check"));
22+
} else {
23+
expect(item.selected).toBe(false);
24+
expect(item.iconPath).toBe(undefined);
25+
}
26+
});
27+
}
28+
29+
describe("getChildren", () => {
30+
const app = createMockApp({
31+
createEventEmitter: <T>() => new EventEmitter<T>(),
32+
});
33+
const languageContext = new LanguageContextStore(app);
34+
const dataProvider = new LanguageSelectionTreeDataProvider(languageContext);
35+
36+
it("returns list of all languages", async () => {
37+
const expectedLanguageNames = [
38+
"All languages",
39+
...Object.values(QueryLanguage).map((language) => {
40+
return getLanguageDisplayName(language);
41+
}),
42+
];
43+
const actualLanguagesNames = dataProvider.getChildren().map((item) => {
44+
return item.label;
45+
});
46+
47+
// Note that the internal order of C# and C / C++ is different from what is shown in the UI.
48+
// So we sort to make sure we can compare the two lists.
49+
expect(actualLanguagesNames.sort()).toEqual(expectedLanguageNames.sort());
50+
});
51+
52+
it("has a default selection of All languages", async () => {
53+
const items = dataProvider.getChildren();
54+
expectSelected(items, undefined);
55+
});
56+
57+
it("changes the selected element when the language is changed", async () => {
58+
await languageContext.setLanguageContext(QueryLanguage.CSharp);
59+
const items = dataProvider.getChildren();
60+
expectSelected(items, QueryLanguage.CSharp);
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)