Merge pull request #2857 from github/charisk/initial-modeling-store

Introduce modeling store and move some state there
This commit is contained in:
Charis Kyriakou
2023-09-26 11:36:46 +01:00
committed by GitHub
7 changed files with 262 additions and 81 deletions

View File

@@ -7,12 +7,16 @@ import {
import { Method, Usage } from "../method";
import { DatabaseItem } from "../../databases/local-databases";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { ModelingStore } from "../modeling-store";
export class MethodsUsagePanel extends DisposableObject {
private readonly dataProvider: MethodsUsageDataProvider;
private readonly treeView: TreeView<MethodsUsageTreeViewItem>;
public constructor(cliServer: CodeQLCliServer) {
public constructor(
private readonly modelingStore: ModelingStore,
cliServer: CodeQLCliServer,
) {
super();
this.dataProvider = new MethodsUsageDataProvider(cliServer);
@@ -21,6 +25,8 @@ export class MethodsUsagePanel extends DisposableObject {
treeDataProvider: this.dataProvider,
});
this.push(this.treeView);
this.registerToModelingStoreEvents();
}
public async setState(
@@ -44,4 +50,39 @@ export class MethodsUsagePanel extends DisposableObject {
await this.treeView.reveal(canonicalUsage);
}
}
private registerToModelingStoreEvents(): void {
this.push(
this.modelingStore.onActiveDbChanged(async () => {
await this.handleStateChangeEvent();
}),
);
this.push(
this.modelingStore.onMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
}),
);
this.push(
this.modelingStore.onHideModeledMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
}),
);
}
private async handleStateChangeEvent(): Promise<void> {
const activeState = this.modelingStore.getStateForActiveDb();
if (activeState !== undefined) {
await this.setState(
activeState.methods,
activeState.databaseItem,
activeState.hideModeledMethods,
);
}
}
}

View File

@@ -19,16 +19,16 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
import { Method, Usage } from "./method";
import { setUpPack } from "./model-editor-queries";
import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
import { ModelingStore } from "./modeling-store";
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
export class ModelEditorModule extends DisposableObject {
private readonly queryStorageDir: string;
private readonly modelingStore: ModelingStore;
private readonly methodsUsagePanel: MethodsUsagePanel;
private readonly methodModelingPanel: MethodModelingPanel;
private mostRecentlyActiveView: ModelEditorView | undefined = undefined;
private constructor(
private readonly app: App,
private readonly databaseManager: DatabaseManager,
@@ -38,24 +38,13 @@ export class ModelEditorModule extends DisposableObject {
) {
super();
this.queryStorageDir = join(baseQueryStorageDir, "model-editor-results");
this.methodsUsagePanel = this.push(new MethodsUsagePanel(cliServer));
this.modelingStore = new ModelingStore(app);
this.methodsUsagePanel = this.push(
new MethodsUsagePanel(this.modelingStore, cliServer),
);
this.methodModelingPanel = this.push(new MethodModelingPanel(app));
}
private handleViewBecameActive(view: ModelEditorView): void {
this.mostRecentlyActiveView = view;
}
private handleViewWasDisposed(view: ModelEditorView): void {
if (this.mostRecentlyActiveView === view) {
this.mostRecentlyActiveView = undefined;
}
}
private isMostRecentlyActiveView(view: ModelEditorView): boolean {
return this.mostRecentlyActiveView === view;
}
public static async initialize(
app: App,
databaseManager: DatabaseManager,
@@ -153,6 +142,7 @@ export class ModelEditorModule extends DisposableObject {
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.databaseManager,
this.cliServer,
this.queryRunner,
@@ -161,16 +151,15 @@ export class ModelEditorModule extends DisposableObject {
db,
modelFile,
Mode.Application,
this.methodsUsagePanel.setState.bind(this.methodsUsagePanel),
this.showMethod.bind(this),
this.handleViewBecameActive.bind(this),
(view) => {
this.handleViewWasDisposed(view);
void cleanupQueryDir();
},
this.isMostRecentlyActiveView.bind(this),
);
this.modelingStore.onDbClosed(async (dbUri) => {
if (dbUri === db.databaseUri.toString()) {
await cleanupQueryDir();
}
});
this.push(view);
this.push({
dispose(): void {

View File

@@ -41,8 +41,8 @@ import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
import { pickExtensionPack } from "./extension-pack-picker";
import { getLanguageDisplayName } from "../common/query-language";
import { AutoModeler } from "./auto-modeler";
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
import { telemetryListener } from "../common/vscode/telemetry";
import { ModelingStore } from "./modeling-store";
export class ModelEditorView extends AbstractWebview<
ToModelEditorMessage,
@@ -50,11 +50,9 @@ export class ModelEditorView extends AbstractWebview<
> {
private readonly autoModeler: AutoModeler;
private methods: Method[];
private hideModeledMethods: boolean;
public constructor(
protected readonly app: App,
private readonly modelingStore: ModelingStore,
private readonly databaseManager: DatabaseManager,
private readonly cliServer: CodeQLCliServer,
private readonly queryRunner: QueryRunner,
@@ -63,23 +61,16 @@ export class ModelEditorView extends AbstractWebview<
private readonly databaseItem: DatabaseItem,
private readonly extensionPack: ExtensionPack,
private mode: Mode,
private readonly updateMethodsUsagePanelState: (
methods: Method[],
databaseItem: DatabaseItem,
hideModeledMethods: boolean,
) => Promise<void>,
private readonly showMethod: (
method: Method,
usage: Usage,
) => Promise<void>,
private readonly handleViewBecameActive: (view: ModelEditorView) => void,
private readonly handleViewWasDisposed: (view: ModelEditorView) => void,
private readonly isMostRecentlyActiveView: (
view: ModelEditorView,
) => boolean,
) {
super(app);
this.modelingStore.initializeStateForDb(databaseItem);
this.registerToModelingStoreEvents();
this.autoModeler = new AutoModeler(
app,
cliServer,
@@ -97,8 +88,6 @@ export class ModelEditorView extends AbstractWebview<
await this.postMessage({ t: "addModeledMethods", modeledMethods });
},
);
this.methods = [];
this.hideModeledMethods = INITIAL_HIDE_MODELED_METHODS_VALUE;
}
public async openView() {
@@ -107,12 +96,7 @@ export class ModelEditorView extends AbstractWebview<
panel.onDidChangeViewState(async () => {
if (panel.active) {
this.handleViewBecameActive(this);
await this.updateMethodsUsagePanelState(
this.methods,
this.databaseItem,
this.hideModeledMethods,
);
this.modelingStore.setActiveDb(this.databaseItem);
await this.markModelEditorAsActive();
} else {
await this.updateModelEditorActiveContext();
@@ -120,7 +104,7 @@ export class ModelEditorView extends AbstractWebview<
});
panel.onDidDispose(() => {
this.handleViewWasDisposed(this);
this.modelingStore.removeDb(this.databaseItem);
// onDidDispose is called after the tab has been closed,
// so we want to check if there are any others still open.
void this.app.commands.execute(
@@ -309,11 +293,11 @@ export class ModelEditorView extends AbstractWebview<
break;
case "switchMode":
this.mode = msg.mode;
this.methods = [];
this.modelingStore.setMethods(this.databaseItem, []);
await Promise.all([
this.postMessage({
t: "setMethods",
methods: this.methods,
methods: [],
}),
this.setViewState(),
withProgress((progress) => this.loadMethods(progress), {
@@ -324,11 +308,9 @@ export class ModelEditorView extends AbstractWebview<
break;
case "hideModeledMethods":
this.hideModeledMethods = msg.hideModeledMethods;
await this.updateMethodsUsagePanelState(
this.methods,
this.modelingStore.setHideModeledMethods(
this.databaseItem,
this.hideModeledMethods,
msg.hideModeledMethods,
);
void telemetryListener?.sendUIInteraction(
"model-editor-hide-modeled-methods",
@@ -409,19 +391,8 @@ export class ModelEditorView extends AbstractWebview<
if (!queryResult) {
return;
}
this.methods = queryResult;
await this.postMessage({
t: "setMethods",
methods: this.methods,
});
if (this.isMostRecentlyActiveView(this)) {
await this.updateMethodsUsagePanelState(
this.methods,
this.databaseItem,
this.hideModeledMethods,
);
}
this.modelingStore.setMethods(this.databaseItem, queryResult);
} catch (err) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
@@ -527,6 +498,7 @@ export class ModelEditorView extends AbstractWebview<
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.databaseManager,
this.cliServer,
this.queryRunner,
@@ -535,11 +507,7 @@ export class ModelEditorView extends AbstractWebview<
addedDatabase,
modelFile,
Mode.Framework,
this.updateMethodsUsagePanelState,
this.showMethod,
this.handleViewBecameActive,
this.handleViewWasDisposed,
this.isMostRecentlyActiveView,
);
await view.openView();
});
@@ -617,4 +585,17 @@ export class ModelEditorView extends AbstractWebview<
return addedDatabase;
}
private registerToModelingStoreEvents() {
this.push(
this.modelingStore.onMethodsChanged(async (event) => {
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
await this.postMessage({
t: "setMethods",
methods: event.methods,
});
}
}),
);
}
}

View File

@@ -0,0 +1,141 @@
import { App } from "../common/app";
import { DisposableObject } from "../common/disposable-object";
import { AppEvent, AppEventEmitter } from "../common/events";
import { DatabaseItem } from "../databases/local-databases";
import { Method } from "./method";
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
interface DbModelingState {
databaseItem: DatabaseItem;
methods: Method[];
hideModeledMethods: boolean;
}
interface MethodsChangedEvent {
methods: Method[];
dbUri: string;
isActiveDb: boolean;
}
interface HideModeledMethodsChangedEvent {
hideModeledMethods: boolean;
isActiveDb: boolean;
}
export class ModelingStore extends DisposableObject {
public readonly onActiveDbChanged: AppEvent<void>;
public readonly onDbClosed: AppEvent<string>;
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
private readonly state: Map<string, DbModelingState>;
private activeDb: string | undefined;
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
constructor(app: App) {
super();
// State initialization
this.state = new Map<string, DbModelingState>();
// Event initialization
this.onActiveDbChangedEventEmitter = this.push(
app.createEventEmitter<void>(),
);
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
this.onDbClosed = this.onDbClosedEventEmitter.event;
this.onMethodsChangedEventEmitter = this.push(
app.createEventEmitter<MethodsChangedEvent>(),
);
this.onMethodsChanged = this.onMethodsChangedEventEmitter.event;
this.onHideModeledMethodsChangedEventEmitter = this.push(
app.createEventEmitter<HideModeledMethodsChangedEvent>(),
);
this.onHideModeledMethodsChanged =
this.onHideModeledMethodsChangedEventEmitter.event;
}
public initializeStateForDb(databaseItem: DatabaseItem) {
const dbUri = databaseItem.databaseUri.toString();
this.state.set(dbUri, {
databaseItem,
methods: [],
hideModeledMethods: INITIAL_HIDE_MODELED_METHODS_VALUE,
});
}
public setActiveDb(databaseItem: DatabaseItem) {
this.activeDb = databaseItem.databaseUri.toString();
this.onActiveDbChangedEventEmitter.fire();
}
public removeDb(databaseItem: DatabaseItem) {
const dbUri = databaseItem.databaseUri.toString();
if (!this.state.has(dbUri)) {
throw Error("Cannot remove a database that has not been initialized");
}
if (this.activeDb === dbUri) {
this.activeDb = undefined;
this.onActiveDbChangedEventEmitter.fire();
}
this.state.delete(dbUri);
this.onDbClosedEventEmitter.fire(dbUri);
}
public getStateForActiveDb(): DbModelingState | undefined {
if (!this.activeDb) {
return undefined;
}
return this.state.get(this.activeDb);
}
public setMethods(dbItem: DatabaseItem, methods: Method[]) {
const dbState = this.getState(dbItem);
const dbUri = dbItem.databaseUri.toString();
dbState.methods = methods;
this.onMethodsChangedEventEmitter.fire({
methods,
dbUri,
isActiveDb: dbUri === this.activeDb,
});
}
public setHideModeledMethods(
dbItem: DatabaseItem,
hideModeledMethods: boolean,
) {
const dbState = this.getState(dbItem);
const dbUri = dbItem.databaseUri.toString();
dbState.hideModeledMethods = hideModeledMethods;
this.onHideModeledMethodsChangedEventEmitter.fire({
hideModeledMethods,
isActiveDb: dbUri === this.activeDb,
});
}
private getState(databaseItem: DatabaseItem): DbModelingState {
if (!this.state.has(databaseItem.databaseUri.toString())) {
throw Error(
"Cannot get state for a database that has not been initialized",
);
}
return this.state.get(databaseItem.databaseUri.toString())!;
}
}

View File

@@ -0,0 +1,27 @@
import { mockedObject } from "../../vscode-tests/utils/mocking.helpers";
import { ModelingStore } from "../../../src/model-editor/modeling-store";
export function createMockModelingStore({
initializeStateForDb = jest.fn(),
getStateForActiveDb = jest.fn(),
onActiveDbChanged = jest.fn(),
onDbClosed = jest.fn(),
onMethodsChanged = jest.fn(),
onHideModeledMethodsChanged = jest.fn(),
}: {
initializeStateForDb?: ModelingStore["initializeStateForDb"];
getStateForActiveDb?: ModelingStore["getStateForActiveDb"];
onActiveDbChanged?: ModelingStore["onActiveDbChanged"];
onDbClosed?: ModelingStore["onDbClosed"];
onMethodsChanged?: ModelingStore["onMethodsChanged"];
onHideModeledMethodsChanged?: ModelingStore["onHideModeledMethodsChanged"];
} = {}): ModelingStore {
return mockedObject<ModelingStore>({
initializeStateForDb,
getStateForActiveDb,
onActiveDbChanged,
onDbClosed,
onMethodsChanged,
onHideModeledMethodsChanged,
});
}

View File

@@ -8,6 +8,8 @@ import {
createMethod,
createUsage,
} from "../../../../factories/model-editor/method-factories";
import { ModelingStore } from "../../../../../src/model-editor/modeling-store";
import { createMockModelingStore } from "../../../../__mocks__/model-editor/modelingStoreMock";
describe("MethodsUsagePanel", () => {
const mockCliServer = mockedObject<CodeQLCliServer>({});
@@ -25,7 +27,9 @@ describe("MethodsUsagePanel", () => {
} as TreeView<unknown>;
jest.spyOn(window, "createTreeView").mockReturnValue(mockTreeView);
const panel = new MethodsUsagePanel(mockCliServer);
const modelingStore = createMockModelingStore();
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
expect(mockTreeView.badge?.value).toBe(1);
@@ -34,6 +38,7 @@ describe("MethodsUsagePanel", () => {
describe("revealItem", () => {
let mockTreeView: TreeView<unknown>;
let modelingStore: ModelingStore;
const hideModeledMethods: boolean = false;
const usage = createUsage();
@@ -43,6 +48,8 @@ describe("MethodsUsagePanel", () => {
reveal: jest.fn(),
});
jest.spyOn(window, "createTreeView").mockReturnValue(mockTreeView);
modelingStore = createMockModelingStore();
});
it("should reveal the correct item in the tree view", async () => {
@@ -52,7 +59,7 @@ describe("MethodsUsagePanel", () => {
}),
];
const panel = new MethodsUsagePanel(mockCliServer);
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
await panel.revealItem(usage);
@@ -62,7 +69,7 @@ describe("MethodsUsagePanel", () => {
it("should do nothing if usage cannot be found", async () => {
const methods = [createMethod({})];
const panel = new MethodsUsagePanel(mockCliServer);
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
await panel.revealItem(usage);

View File

@@ -8,9 +8,11 @@ import { createMockApp } from "../../../__mocks__/appMock";
import { mockEmptyDatabaseManager } from "../query-testing/test-runner-helpers";
import { QueryRunner } from "../../../../src/query-server";
import { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
import { createMockModelingStore } from "../../../__mocks__/model-editor/modelingStoreMock";
describe("ModelEditorView", () => {
const app = createMockApp({});
const modelingStore = createMockModelingStore();
const databaseManager = mockEmptyDatabaseManager();
const cliServer = mockedObject<CodeQLCliServer>({});
const queryRunner = mockedObject<QueryRunner>({});
@@ -29,17 +31,14 @@ describe("ModelEditorView", () => {
dataExtensions: ["models/**/*.yml"],
};
const mode = Mode.Application;
const updateMethodsUsagePanelState = jest.fn();
const showMethod = jest.fn();
const handleViewBecameActive = jest.fn();
const handleViewWasDisposed = jest.fn();
const isMostRecentlyActiveView = jest.fn();
let view: ModelEditorView;
beforeEach(() => {
view = new ModelEditorView(
app,
modelingStore,
databaseManager,
cliServer,
queryRunner,
@@ -48,11 +47,7 @@ describe("ModelEditorView", () => {
databaseItem,
extensionPack,
mode,
updateMethodsUsagePanelState,
showMethod,
handleViewBecameActive,
handleViewWasDisposed,
isMostRecentlyActiveView,
);
});