Merge pull request #2935 from github/starcke/language-selection-panel
Add language filter panel.
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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 &
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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\./) ||
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user