Merge pull request #2935 from github/starcke/language-selection-panel

Add language filter panel.
This commit is contained in:
Anders Starcke Henriksen
2023-10-16 09:42:35 +02:00
committed by GitHub
8 changed files with 250 additions and 0 deletions

View File

@@ -561,6 +561,10 @@
"command": "codeQL.copyVersion", "command": "codeQL.copyVersion",
"title": "CodeQL: Copy Version Information" "title": "CodeQL: Copy Version Information"
}, },
{
"command": "codeQLLanguageSelection.setSelectedItem",
"title": "Select"
},
{ {
"command": "codeQLQueries.runLocalQueryFromQueriesPanel", "command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"title": "Run local query", "title": "Run local query",
@@ -1147,6 +1151,11 @@
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/", "when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
"group": "2_qlContextMenu@1" "group": "2_qlContextMenu@1"
}, },
{
"command": "codeQLLanguageSelection.setSelectedItem",
"when": "view == codeQLLanguageSelection && viewItem =~ /canBeSelected/",
"group": "inline"
},
{ {
"command": "codeQLDatabases.setCurrentDatabase", "command": "codeQLDatabases.setCurrentDatabase",
"group": "inline", "group": "inline",
@@ -1495,6 +1504,10 @@
{ {
"command": "codeQL.openModelEditor" "command": "codeQL.openModelEditor"
}, },
{
"command": "codeQLLanguageSelection.setSelectedItem",
"when": "false"
},
{ {
"command": "codeQLQueries.runLocalQueryContextMenu", "command": "codeQLQueries.runLocalQueryContextMenu",
"when": "false" "when": "false"
@@ -1965,6 +1978,11 @@
}, },
"views": { "views": {
"ql-container": [ "ql-container": [
{
"id": "codeQLLanguageSelection",
"name": "Language",
"when": "config.codeQL.canary && config.codeQL.showLanguageFilter"
},
{ {
"id": "codeQLDatabases", "id": "codeQLDatabases",
"name": "Databases" "name": "Databases"

View File

@@ -12,6 +12,7 @@ import type {
} from "../variant-analysis/shared/variant-analysis"; } from "../variant-analysis/shared/variant-analysis";
import type { QLDebugConfiguration } from "../debugger/debug-configuration"; import type { QLDebugConfiguration } from "../debugger/debug-configuration";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item"; import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import type { LanguageSelectionTreeViewItem } from "../language-selection-panel/language-selection-data-provider";
// A command function matching the signature that VS Code calls when // A command function matching the signature that VS Code calls when
// a command is invoked from a context menu on a TreeView with // a command is invoked from a context menu on a TreeView with
@@ -198,6 +199,13 @@ export type QueryHistoryCommands = {
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>; "codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
}; };
// Commands user for the language selector panel
export type LanguageSelectionCommands = {
"codeQLLanguageSelection.setSelectedItem": (
item: LanguageSelectionTreeViewItem,
) => Promise<void>;
};
// Commands used for the local databases panel // Commands used for the local databases panel
export type LocalDatabasesCommands = { export type LocalDatabasesCommands = {
// Command palette commands // Command palette commands
@@ -360,6 +368,7 @@ export type AllExtensionCommands = BaseCommands &
QueryEditorCommands & QueryEditorCommands &
ResultsViewCommands & ResultsViewCommands &
QueryHistoryCommands & QueryHistoryCommands &
LanguageSelectionCommands &
LocalDatabasesCommands & LocalDatabasesCommands &
DebuggerCommands & DebuggerCommands &
VariantAnalysisCommands & VariantAnalysisCommands &

View File

@@ -136,6 +136,7 @@ import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server";
import { QueriesModule } from "./queries-panel/queries-module"; import { QueriesModule } from "./queries-panel/queries-module";
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider"; import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
import { LanguageContextStore } from "./language-context-store"; import { LanguageContextStore } from "./language-context-store";
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
/** /**
* extension.ts * extension.ts
@@ -779,6 +780,10 @@ async function activateWithInstalledDistribution(
void extLogger.log("Initializing language context."); void extLogger.log("Initializing language context.");
const languageContext = new LanguageContextStore(app); const languageContext = new LanguageContextStore(app);
void extLogger.log("Initializing language selector.");
const languageSelectionPanel = new LanguageSelectionPanel(languageContext);
ctx.subscriptions.push(languageSelectionPanel);
void extLogger.log("Initializing database panel."); void extLogger.log("Initializing database panel.");
const databaseUI = new DatabaseUI( const databaseUI = new DatabaseUI(
app, app,
@@ -1016,6 +1021,7 @@ async function activateWithInstalledDistribution(
...getPackagingCommands({ ...getPackagingCommands({
cliServer, cliServer,
}), }),
...languageSelectionPanel.getCommands(),
...modelEditorModule.getCommands(), ...modelEditorModule.getCommands(),
...evalLogViewer.getCommands(), ...evalLogViewer.getCommands(),
...summaryLanguageSupport.getCommands(), ...summaryLanguageSupport.getCommands(),

View File

@@ -43,7 +43,28 @@ export class LanguageContextStore extends DisposableObject {
); );
} }
/**
* This returns true if the given language should be included.
*
* That means that either the given language is selected or the "All" option is selected.
*
* @param language a query language or undefined if the language is unknown.
*/
public shouldInclude(language: QueryLanguage | undefined): boolean { public shouldInclude(language: QueryLanguage | undefined): boolean {
return this.languageFilter === "All" || this.languageFilter === language; return this.languageFilter === "All" || this.languageFilter === language;
} }
/**
* This returns true if the given language is selected.
*
* If no language is given then it returns true if the "All" option is selected.
*
* @param language a query language or undefined.
*/
public isSelectedLanguage(language: QueryLanguage | undefined): boolean {
return (
(this.languageFilter === "All" && language === undefined) ||
this.languageFilter === language
);
}
} }

View File

@@ -0,0 +1,91 @@
import { DisposableObject } from "../common/disposable-object";
import { LanguageContextStore } from "../language-context-store";
import {
Event,
EventEmitter,
ThemeIcon,
TreeDataProvider,
TreeItem,
} from "vscode";
import {
QueryLanguage,
getLanguageDisplayName,
} from "../common/query-language";
const ALL_LANGUAGE_SELECTION_OPTIONS = [
undefined, // All languages
QueryLanguage.Cpp,
QueryLanguage.CSharp,
QueryLanguage.Go,
QueryLanguage.Java,
QueryLanguage.Javascript,
QueryLanguage.Python,
QueryLanguage.Ruby,
QueryLanguage.Swift,
];
// A tree view items consisting of of a language (or undefined for all languages)
// and a boolean indicating whether it is selected or not.
export class LanguageSelectionTreeViewItem extends TreeItem {
constructor(
public readonly language: QueryLanguage | undefined,
public readonly selected: boolean = false,
) {
const label = language ? getLanguageDisplayName(language) : "All languages";
super(label);
this.iconPath = selected ? new ThemeIcon("check") : undefined;
this.contextValue = selected ? undefined : "canBeSelected";
}
}
export class LanguageSelectionTreeDataProvider
extends DisposableObject
implements TreeDataProvider<LanguageSelectionTreeViewItem>
{
private treeItems: LanguageSelectionTreeViewItem[];
private readonly onDidChangeTreeDataEmitter = this.push(
new EventEmitter<void>(),
);
public constructor(private readonly languageContext: LanguageContextStore) {
super();
this.treeItems = this.createTree();
// If the language context changes, we need to update the tree.
this.push(
this.languageContext.onLanguageContextChanged(() => {
this.treeItems = this.createTree();
this.onDidChangeTreeDataEmitter.fire();
}),
);
}
public get onDidChangeTreeData(): Event<void> {
return this.onDidChangeTreeDataEmitter.event;
}
public getTreeItem(item: LanguageSelectionTreeViewItem): TreeItem {
return item;
}
public getChildren(
item?: LanguageSelectionTreeViewItem,
): LanguageSelectionTreeViewItem[] {
if (!item) {
return this.treeItems;
} else {
return [];
}
}
private createTree(): LanguageSelectionTreeViewItem[] {
return ALL_LANGUAGE_SELECTION_OPTIONS.map((language) => {
return new LanguageSelectionTreeViewItem(
language,
this.languageContext.isSelectedLanguage(language),
);
});
}
}

View File

@@ -0,0 +1,41 @@
import { DisposableObject } from "../common/disposable-object";
import { window } from "vscode";
import {
LanguageSelectionTreeDataProvider,
LanguageSelectionTreeViewItem,
} from "./language-selection-data-provider";
import { LanguageContextStore } from "../language-context-store";
import { LanguageSelectionCommands } from "../common/commands";
// This panel allows the selection of a single language, that will
// then filter all other relevant views (e.g. db panel, query history).
export class LanguageSelectionPanel extends DisposableObject {
constructor(private readonly languageContext: LanguageContextStore) {
super();
const dataProvider = new LanguageSelectionTreeDataProvider(languageContext);
this.push(dataProvider);
const treeView = window.createTreeView("codeQLLanguageSelection", {
treeDataProvider: dataProvider,
});
this.push(treeView);
}
public getCommands(): LanguageSelectionCommands {
return {
"codeQLLanguageSelection.setSelectedItem":
this.handleSetSelectedLanguage.bind(this),
};
}
private async handleSetSelectedLanguage(
item: LanguageSelectionTreeViewItem,
): Promise<void> {
if (item.language) {
await this.languageContext.setLanguageContext(item.language);
} else {
await this.languageContext.clearLanguageContext();
}
}
}

View File

@@ -38,6 +38,7 @@ describe("commands declared in package.json", () => {
expect(title).toBeDefined(); expect(title).toBeDefined();
commandTitles[command] = title!; commandTitles[command] = title!;
} else if ( } else if (
command.match(/^codeQLLanguageSelection\./) ||
command.match(/^codeQLDatabases\./) || command.match(/^codeQLDatabases\./) ||
command.match(/^codeQLQueries\./) || command.match(/^codeQLQueries\./) ||
command.match(/^codeQLVariantAnalysisRepositories\./) || command.match(/^codeQLVariantAnalysisRepositories\./) ||

View File

@@ -0,0 +1,63 @@
import {
QueryLanguage,
getLanguageDisplayName,
} from "../../../../src/common/query-language";
import { LanguageContextStore } from "../../../../src/language-context-store";
import {
LanguageSelectionTreeDataProvider,
LanguageSelectionTreeViewItem,
} from "../../../../src/language-selection-panel/language-selection-data-provider";
import { createMockApp } from "../../../__mocks__/appMock";
import { EventEmitter, ThemeIcon } from "vscode";
describe("LanguageSelectionTreeDataProvider", () => {
function expectSelected(
items: LanguageSelectionTreeViewItem[],
expected: QueryLanguage | undefined,
) {
items.forEach((item) => {
if (item.language === expected) {
expect(item.selected).toBe(true);
expect(item.iconPath).toEqual(new ThemeIcon("check"));
} else {
expect(item.selected).toBe(false);
expect(item.iconPath).toBe(undefined);
}
});
}
describe("getChildren", () => {
const app = createMockApp({
createEventEmitter: <T>() => new EventEmitter<T>(),
});
const languageContext = new LanguageContextStore(app);
const dataProvider = new LanguageSelectionTreeDataProvider(languageContext);
it("returns list of all languages", async () => {
const expectedLanguageNames = [
"All languages",
...Object.values(QueryLanguage).map((language) => {
return getLanguageDisplayName(language);
}),
];
const actualLanguagesNames = dataProvider.getChildren().map((item) => {
return item.label;
});
// Note that the internal order of C# and C / C++ is different from what is shown in the UI.
// So we sort to make sure we can compare the two lists.
expect(actualLanguagesNames.sort()).toEqual(expectedLanguageNames.sort());
});
it("has a default selection of All languages", async () => {
const items = dataProvider.getChildren();
expectSelected(items, undefined);
});
it("changes the selected element when the language is changed", async () => {
await languageContext.setLanguageContext(QueryLanguage.CSharp);
const items = dataProvider.getChildren();
expectSelected(items, QueryLanguage.CSharp);
});
});
});