diff --git a/src/extension/main.ts b/src/extension/main.ts index d014d19..5f93827 100644 --- a/src/extension/main.ts +++ b/src/extension/main.ts @@ -328,7 +328,7 @@ export const activate = (context: vscode.ExtensionContext) => { statusBarCreator, store: appStore, }); - const treeProvider = CommandTreeProvider.create({ configManager, configReader, eventBus }); + const treeProvider = CommandTreeProvider.create({ store: appStore }); const importExportManager = ImportExportManager.create({ configManager, configReader, @@ -346,8 +346,6 @@ export const activate = (context: vscode.ExtensionContext) => { eventBus, }); - treeProvider.setButtonSetManager(buttonSetManager); - const webviewProvider = new ConfigWebviewProvider( context.extensionUri, configReader, diff --git a/src/internal/providers/command-tree-provider.spec.ts b/src/internal/providers/command-tree-provider.spec.ts index f7cea68..1384c4c 100644 --- a/src/internal/providers/command-tree-provider.spec.ts +++ b/src/internal/providers/command-tree-provider.spec.ts @@ -1,5 +1,5 @@ import { ButtonConfig } from "../../pkg/types"; -import { EventBus } from "../event-bus"; +import { createAppStore, AppStoreInstance } from "../stores"; import { createTreeItems } from "../utils/ui-items"; import { CommandTreeItem, CommandTreeProvider, GroupTreeItem } from "./command-tree-provider"; @@ -261,204 +261,185 @@ describe("command-tree-provider", () => { }); describe("CommandTreeProvider", () => { - const mockConfigReader = jest.fn(); - const mockConfigManager = { - getButtonsWithFallback: jest.fn(), - }; + let mockStore: AppStoreInstance; + let provider: CommandTreeProvider; beforeEach(() => { - jest.clearAllMocks(); + mockStore = createAppStore(); + mockStore.getState().setButtons([ + { command: "echo test1", id: "btn-1", name: "Test Button 1" }, + { command: "echo test2", id: "btn-2", name: "Test Button 2" }, + ]); }); - describe("constructor", () => { - it("should create provider without EventBus", () => { - const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any); + afterEach(() => { + if (provider) { + provider.dispose(); + } + }); + + describe("static create", () => { + it("should create provider without store (uses global store)", () => { + provider = CommandTreeProvider.create(); expect(provider).toBeInstanceOf(CommandTreeProvider); }); - it("should create provider with EventBus and subscribe to events", () => { - const eventBus = new EventBus(); - const onSpy = jest.spyOn(eventBus, "on"); - - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + it("should create provider with custom store", () => { + provider = CommandTreeProvider.create({ store: mockStore }); expect(provider).toBeInstanceOf(CommandTreeProvider); - expect(onSpy).toHaveBeenCalledWith("buttonSet:created", expect.any(Function)); - expect(onSpy).toHaveBeenCalledWith("buttonSet:deleted", expect.any(Function)); - expect(onSpy).toHaveBeenCalledWith("buttonSet:renamed", expect.any(Function)); - expect(onSpy).toHaveBeenCalledWith("buttonSet:switched", expect.any(Function)); - expect(onSpy).toHaveBeenCalledWith("config:changed", expect.any(Function)); - expect(onSpy).toHaveBeenCalledTimes(5); - - provider.dispose(); - eventBus.dispose(); }); }); - describe("static create", () => { - it("should create provider without EventBus", () => { - const provider = CommandTreeProvider.create({ - configManager: mockConfigManager as any, - configReader: mockConfigReader as any, - }); + describe("Store subscription", () => { + it("should subscribe to store on creation", () => { + const subscribeSpy = jest.spyOn(mockStore, "subscribe"); - expect(provider).toBeInstanceOf(CommandTreeProvider); - provider.dispose(); + provider = CommandTreeProvider.create({ store: mockStore }); + + expect(subscribeSpy).toHaveBeenCalled(); }); - it("should create provider with EventBus", () => { - const eventBus = new EventBus(); - const provider = CommandTreeProvider.create({ - configManager: mockConfigManager as any, - configReader: mockConfigReader as any, - eventBus, - }); + it("should call refresh when store buttons change", () => { + provider = CommandTreeProvider.create({ store: mockStore }); - expect(provider).toBeInstanceOf(CommandTreeProvider); + const refreshSpy = jest.spyOn(provider, "refresh"); - provider.dispose(); - eventBus.dispose(); - }); - }); + mockStore + .getState() + .setButtons([{ command: "echo new", id: "new-btn", name: "New Button" }]); - describe("event subscription", () => { - it("should refresh when config:changed event is emitted", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + expect(refreshSpy).toHaveBeenCalled(); + }); - const refreshSpy = jest.spyOn(provider, "refresh"); + it("should use buttons from store for tree items", async () => { + mockStore + .getState() + .setButtons([{ command: "echo store", id: "store-btn", name: "Store Button" }]); - eventBus.emit("config:changed", { scope: "workspace" }); + provider = CommandTreeProvider.create({ store: mockStore }); - expect(refreshSpy).toHaveBeenCalledTimes(1); + const items = await provider.getChildren(); - provider.dispose(); - eventBus.dispose(); + expect(items).toHaveLength(1); + expect(items[0].label).toBe("Store Button"); + expect((items[0] as CommandTreeItem).commandString).toBe("echo store"); }); - it("should refresh when buttonSet:switched event is emitted", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + it("should reflect store changes in tree items", async () => { + provider = CommandTreeProvider.create({ store: mockStore }); - const refreshSpy = jest.spyOn(provider, "refresh"); - - eventBus.emit("buttonSet:switched", { setName: "test-set" }); + // Initial state + let items = await provider.getChildren(); + expect(items).toHaveLength(2); - expect(refreshSpy).toHaveBeenCalledTimes(1); + // Update store + mockStore + .getState() + .setButtons([{ command: "echo updated", id: "updated-btn", name: "Updated Button" }]); - provider.dispose(); - eventBus.dispose(); + // Check updated items + items = await provider.getChildren(); + expect(items).toHaveLength(1); + expect(items[0].label).toBe("Updated Button"); }); + }); - it("should refresh multiple times for multiple events", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); - - const refreshSpy = jest.spyOn(provider, "refresh"); + describe("getChildren", () => { + it("should return root items when no element provided", async () => { + provider = CommandTreeProvider.create({ store: mockStore }); - eventBus.emit("config:changed", { scope: "workspace" }); - eventBus.emit("buttonSet:switched", { setName: "set1" }); - eventBus.emit("config:changed", { scope: "global" }); - eventBus.emit("buttonSet:switched", { setName: "set2" }); + const items = await provider.getChildren(); - expect(refreshSpy).toHaveBeenCalledTimes(4); + expect(items).toHaveLength(2); + expect(items[0]).toBeInstanceOf(CommandTreeItem); + expect(items[1]).toBeInstanceOf(CommandTreeItem); + }); - provider.dispose(); - eventBus.dispose(); + it("should return group children for GroupTreeItem", async () => { + mockStore.getState().setButtons([ + { + group: [ + { command: "echo sub1", id: "sub-1", name: "Sub 1" }, + { command: "echo sub2", id: "sub-2", name: "Sub 2" }, + ], + id: "group-1", + name: "Test Group", + }, + ]); + + provider = CommandTreeProvider.create({ store: mockStore }); + + const rootItems = await provider.getChildren(); + expect(rootItems).toHaveLength(1); + expect(rootItems[0]).toBeInstanceOf(GroupTreeItem); + + const groupChildren = await provider.getChildren(rootItems[0] as GroupTreeItem); + expect(groupChildren).toHaveLength(2); + expect(groupChildren[0].label).toBe("Sub 1"); + expect(groupChildren[1].label).toBe("Sub 2"); }); - it("should not refresh after dispose", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + it("should return empty array for CommandTreeItem", async () => { + provider = CommandTreeProvider.create({ store: mockStore }); - const refreshSpy = jest.spyOn(provider, "refresh"); + const rootItems = await provider.getChildren(); + const commandItem = rootItems[0] as CommandTreeItem; - provider.dispose(); + const children = await provider.getChildren(commandItem); + expect(children).toEqual([]); + }); - eventBus.emit("config:changed", { scope: "workspace" }); - eventBus.emit("buttonSet:switched", { setName: "test-set" }); + it("should handle empty buttons", async () => { + mockStore.getState().setButtons([]); - expect(refreshSpy).not.toHaveBeenCalled(); + provider = CommandTreeProvider.create({ store: mockStore }); - eventBus.dispose(); + const items = await provider.getChildren(); + expect(items).toHaveLength(0); }); + }); - it("should not subscribe to events when EventBus is not provided", () => { - const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any); - - const refreshSpy = jest.spyOn(provider, "refresh"); + describe("getTreeItem", () => { + it("should return the element itself", () => { + provider = CommandTreeProvider.create({ store: mockStore }); - // No events to emit since no EventBus, just verify no crash - expect(refreshSpy).not.toHaveBeenCalled(); + const item = new CommandTreeItem("Test", "echo test", false, undefined, "Test"); + const result = provider.getTreeItem(item); - provider.dispose(); + expect(result).toBe(item); }); }); - describe("dispose", () => { - it("should unsubscribe all event handlers", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); - - provider.dispose(); + describe("refresh", () => { + it("should fire onDidChangeTreeData event", () => { + provider = CommandTreeProvider.create({ store: mockStore }); - // Verify unsubscribe was called by checking that events no longer trigger refresh - const refreshSpy = jest.spyOn(provider, "refresh"); - eventBus.emit("config:changed", { scope: "workspace" }); + // Access private EventEmitter for testing + const emitter = (provider as any)._onDidChangeTreeData; + const fireSpy = jest.spyOn(emitter, "fire"); - expect(refreshSpy).not.toHaveBeenCalled(); + provider.refresh(); - eventBus.dispose(); + expect(fireSpy).toHaveBeenCalled(); }); + }); - it("should clear unsubscribers array", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + describe("dispose", () => { + it("should unsubscribe from store when disposed", () => { + provider = CommandTreeProvider.create({ store: mockStore }); provider.dispose(); - // Dispose again should not throw - expect(() => provider.dispose()).not.toThrow(); + const refreshSpy = jest.spyOn(provider, "refresh"); + mockStore.getState().setButtons([]); - eventBus.dispose(); + expect(refreshSpy).not.toHaveBeenCalled(); }); it("should dispose EventEmitter", () => { - const eventBus = new EventBus(); - const provider = new CommandTreeProvider( - mockConfigReader as any, - mockConfigManager as any, - eventBus - ); + provider = CommandTreeProvider.create({ store: mockStore }); // Access private field for testing const emitter = (provider as any)._onDidChangeTreeData; @@ -467,13 +448,12 @@ describe("command-tree-provider", () => { provider.dispose(); expect(disposeSpy).toHaveBeenCalledTimes(1); - - eventBus.dispose(); }); - it("should work without EventBus", () => { - const provider = new CommandTreeProvider(mockConfigReader as any, mockConfigManager as any); + it("should not throw when disposed multiple times", () => { + provider = CommandTreeProvider.create({ store: mockStore }); + provider.dispose(); expect(() => provider.dispose()).not.toThrow(); }); }); diff --git a/src/internal/providers/command-tree-provider.ts b/src/internal/providers/command-tree-provider.ts index c242d08..37d687c 100644 --- a/src/internal/providers/command-tree-provider.ts +++ b/src/internal/providers/command-tree-provider.ts @@ -1,36 +1,24 @@ import * as vscode from "vscode"; -import { ConfigReader, TerminalExecutor } from "../adapters"; -import { EventBus } from "../event-bus"; -import { ButtonSetManager } from "../managers/button-set-manager"; -import { ConfigManager } from "../managers/config-manager"; +import { TerminalExecutor } from "../adapters"; +import { AppStoreInstance, getAppStore } from "../stores"; import { CommandTreeItem, GroupTreeItem, TreeItem, createTreeItems } from "../utils/ui-items"; export { CommandTreeItem, GroupTreeItem }; export class CommandTreeProvider implements vscode.TreeDataProvider, vscode.Disposable { - private _onDidChangeTreeData = new vscode.EventEmitter(); + private readonly _onDidChangeTreeData = new vscode.EventEmitter< + TreeItem | undefined | null | void + >(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - private buttonSetManager?: ButtonSetManager; - private readonly unsubscribers: (() => void)[] = []; + private readonly store: AppStoreInstance; + private storeUnsubscribe?: () => void; - constructor( - private configReader: ConfigReader, - private configManager: ConfigManager, - private eventBus?: EventBus - ) { - if (eventBus) { - this.subscribeToEvents(); - } + private constructor(store?: AppStoreInstance) { + this.store = store ?? getAppStore(); + this.setupStoreSubscription(); } - static create = ({ - configManager, - configReader, - eventBus, - }: { - configManager: ConfigManager; - configReader: ConfigReader; - eventBus?: EventBus; - }): CommandTreeProvider => new CommandTreeProvider(configReader, configManager, eventBus); + static create = (deps?: { store?: AppStoreInstance }): CommandTreeProvider => + new CommandTreeProvider(deps?.store); static executeFromTree = (item: CommandTreeItem, terminalExecutor: TerminalExecutor) => { terminalExecutor(item.commandString, item.useVsCodeApi, item.terminalName, item.buttonName); @@ -38,10 +26,8 @@ export class CommandTreeProvider implements vscode.TreeDataProvider, v dispose = (): void => { this._onDidChangeTreeData.dispose(); - for (const unsubscribe of this.unsubscribers) { - unsubscribe(); - } - this.unsubscribers.length = 0; + this.storeUnsubscribe?.(); + this.storeUnsubscribe = undefined; }; getChildren = (element?: TreeItem): Thenable => { @@ -62,33 +48,21 @@ export class CommandTreeProvider implements vscode.TreeDataProvider, v this._onDidChangeTreeData.fire(); }; - setButtonSetManager = (manager: ButtonSetManager): void => { - this.buttonSetManager = manager; - }; - private getRootItems = (): TreeItem[] => { - const activeSetButtons = this.buttonSetManager?.getButtonsForActiveSet(); - if (activeSetButtons) { - return createTreeItems(activeSetButtons); - } - - const { buttons } = this.configManager.getButtonsWithFallback(this.configReader); + const buttons = this.store.getState().buttons; return createTreeItems(buttons); }; - private subscribeToEvents = (): void => { - if (!this.eventBus) { - return; - } - - const refresh = () => this.refresh(); - - this.unsubscribers.push( - this.eventBus.on("buttonSet:created", refresh), - this.eventBus.on("buttonSet:deleted", refresh), - this.eventBus.on("buttonSet:renamed", refresh), - this.eventBus.on("buttonSet:switched", refresh), - this.eventBus.on("config:changed", refresh) + private setupStoreSubscription = (): void => { + this.storeUnsubscribe = this.store.subscribe( + (state) => state.buttons, + () => { + try { + this.refresh(); + } catch (error) { + console.error("[CommandTreeProvider] Failed to refresh tree view:", error); + } + } ); }; }