diff --git a/src/extension/main.ts b/src/extension/main.ts index 623d0e8..d014d19 100644 --- a/src/extension/main.ts +++ b/src/extension/main.ts @@ -324,10 +324,9 @@ export const activate = (context: vscode.ExtensionContext) => { const terminalManager = TerminalManager.create(eventBus); const configManager = ConfigManager.create({ configWriter, eventBus, localStorage }); const statusBarManager = StatusBarManager.create({ - configManager, configReader, - eventBus, statusBarCreator, + store: appStore, }); const treeProvider = CommandTreeProvider.create({ configManager, configReader, eventBus }); const importExportManager = ImportExportManager.create({ @@ -347,7 +346,6 @@ export const activate = (context: vscode.ExtensionContext) => { eventBus, }); - statusBarManager.setButtonSetManager(buttonSetManager); treeProvider.setButtonSetManager(buttonSetManager); const webviewProvider = new ConfigWebviewProvider( diff --git a/src/internal/managers/status-bar-manager.spec.ts b/src/internal/managers/status-bar-manager.spec.ts index aceb589..1a88e08 100644 --- a/src/internal/managers/status-bar-manager.spec.ts +++ b/src/internal/managers/status-bar-manager.spec.ts @@ -1,5 +1,5 @@ import { ButtonConfig } from "../../pkg/types"; -import { EventBus } from "../event-bus"; +import { createAppStore, AppStoreInstance } from "../stores"; import { calculateButtonPriority, createTooltipText, @@ -340,8 +340,7 @@ describe("status-bar-manager", () => { describe("StatusBarManager", () => { let mockConfigReader: any; let mockStatusBarCreator: any; - let mockConfigManager: any; - let eventBus: EventBus; + let mockStore: AppStoreInstance; let statusBarManager: StatusBarManager; // Mock vscode module to avoid undefined errors @@ -367,166 +366,114 @@ describe("status-bar-manager", () => { }), }; - mockStatusBarCreator = jest.fn().mockReturnValue({ + mockStatusBarCreator = jest.fn().mockImplementation(() => ({ color: "", command: "", dispose: jest.fn(), show: jest.fn(), text: "", tooltip: "", - }); - - mockConfigManager = { - getButtonsWithFallback: jest.fn().mockReturnValue({ - buttons: [ - { command: "echo test1", id: "btn-1", name: "Test Button 1" }, - { command: "echo test2", id: "btn-2", name: "Test Button 2" }, - ], - }), - }; + })); - eventBus = new EventBus(); + 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" }, + ]); }); afterEach(() => { if (statusBarManager) { statusBarManager.dispose(); } - eventBus.dispose(); }); - describe("EventBus integration", () => { - it("should subscribe to config:changed event when eventBus is provided", () => { - const eventBusSpy = jest.spyOn(eventBus, "on"); - - statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, - configReader: mockConfigReader, - eventBus, - statusBarCreator: mockStatusBarCreator, - }); - - expect(eventBusSpy).toHaveBeenCalledWith("config:changed", expect.any(Function)); - expect(eventBusSpy).toHaveBeenCalledWith("buttonSet:switched", expect.any(Function)); - }); + describe("Store subscription", () => { + it("should subscribe to store on creation", () => { + const subscribeSpy = jest.spyOn(mockStore, "subscribe"); - it("should not subscribe to events when eventBus is not provided", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); - // Should not throw errors - manager should handle missing eventBus gracefully - expect(statusBarManager).toBeDefined(); + expect(subscribeSpy).toHaveBeenCalled(); }); - it("should call refreshButtons when config:changed event is emitted", () => { + it("should call refreshButtons when store buttons change", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, - eventBus, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); const refreshSpy = jest.spyOn(statusBarManager, "refreshButtons"); - eventBus.emit("config:changed", { scope: "local" }); + mockStore + .getState() + .setButtons([{ command: "echo new", id: "new-btn", name: "New Button" }]); expect(refreshSpy).toHaveBeenCalled(); }); - it("should call refreshButtons when buttonSet:switched event is emitted", () => { + it("should call refreshButtons when activeSet changes", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, - eventBus, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); const refreshSpy = jest.spyOn(statusBarManager, "refreshButtons"); - eventBus.emit("buttonSet:switched", { setName: "Frontend" }); + mockStore.getState().setActiveSet("Frontend"); expect(refreshSpy).toHaveBeenCalled(); }); - it("should unsubscribe from events when disposed", () => { - statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, - configReader: mockConfigReader, - eventBus, - statusBarCreator: mockStatusBarCreator, - }); - - const refreshSpy = jest.spyOn(statusBarManager, "refreshButtons"); - - // Dispose the manager - statusBarManager.dispose(); - - // Clear the spy to reset call count - refreshSpy.mockClear(); - - // Emit events after disposal - eventBus.emit("config:changed", { scope: "workspace" }); - eventBus.emit("buttonSet:switched", { setName: null }); + it("should use buttons from store for status bar", () => { + mockStore + .getState() + .setButtons([{ command: "echo store", id: "store-btn", name: "Store Button" }]); - // refreshButtons should not be called after disposal - expect(refreshSpy).not.toHaveBeenCalled(); - }); - - it("should handle multiple event emissions", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, - eventBus, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); - // Mock refreshButtons to avoid vscode module issues - const refreshSpy = jest.spyOn(statusBarManager, "refreshButtons").mockImplementation(() => { - // No-op for test - }); - - eventBus.emit("config:changed", { scope: "local" }); - eventBus.emit("buttonSet:switched", { setName: "Backend" }); - eventBus.emit("config:changed", { scope: "global" }); + statusBarManager.refreshButtons(); - expect(refreshSpy).toHaveBeenCalledTimes(3); + const createdItems = mockStatusBarCreator.mock.results.map((r: any) => r.value); + const buttonItem = createdItems.find((item: any) => item.text === "Store Button"); + expect(buttonItem).toBeDefined(); }); - it("should preserve button set manager reference after event refresh", () => { - const mockButtonSetManager = { - getActiveSet: jest.fn().mockReturnValue("TestSet"), - getButtonsForActiveSet: jest - .fn() - .mockReturnValue([{ command: "echo set", id: "set-btn", name: "Set Button" }]), - }; + it("should display activeSet from store in set indicator", () => { + mockStore.getState().setActiveSet("TestSet"); statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, - eventBus, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); - // Set up the button set manager - statusBarManager.setButtonSetManager(mockButtonSetManager as any); - - // Manually trigger refresh to verify button set manager is used + // Call refreshButtons to create status bar items + mockStatusBarCreator.mockClear(); statusBarManager.refreshButtons(); - // Verify that button set manager is still accessible after refresh - expect(mockButtonSetManager.getButtonsForActiveSet).toHaveBeenCalled(); + const createdItems = mockStatusBarCreator.mock.results.map((r: any) => r.value); + const setIndicator = createdItems.find((item: any) => item.text === "$(layers) [TestSet]"); + expect(setIndicator).toBeDefined(); }); }); describe("dispose", () => { it("should dispose all status bar items", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); statusBarManager.refreshButtons(); @@ -540,19 +487,17 @@ describe("status-bar-manager", () => { }); }); - it("should clear unsubscribers array", () => { + it("should unsubscribe from store when disposed", () => { statusBarManager = StatusBarManager.create({ - configManager: mockConfigManager, configReader: mockConfigReader, - eventBus, statusBarCreator: mockStatusBarCreator, + store: mockStore, }); statusBarManager.dispose(); - // Verify unsubscribers were cleared by checking events don't trigger after disposal const refreshSpy = jest.spyOn(statusBarManager, "refreshButtons"); - eventBus.emit("config:changed", { scope: "local" }); + mockStore.getState().setButtons([]); expect(refreshSpy).not.toHaveBeenCalled(); }); diff --git a/src/internal/managers/status-bar-manager.ts b/src/internal/managers/status-bar-manager.ts index 773b821..fb5f041 100644 --- a/src/internal/managers/status-bar-manager.ts +++ b/src/internal/managers/status-bar-manager.ts @@ -2,12 +2,15 @@ import * as vscode from "vscode"; import { ButtonConfig } from "../../pkg/types"; import { COMMANDS } from "../../shared/constants"; import { ConfigReader, StatusBarCreator } from "../adapters"; -import { EventBus } from "../event-bus"; -import { ButtonSetManager } from "./button-set-manager"; -import { ConfigManager } from "./config-manager"; +import { AppStoreInstance, getAppStore } from "../stores"; const SET_INDICATOR_PRIORITY = 1002; +type StoreSlice = { activeSet: string | null; buttons: ButtonConfig[] }; + +const shallowEqual = (a: StoreSlice, b: StoreSlice): boolean => + a.activeSet === b.activeSet && a.buttons === b.buttons; + export const calculateButtonPriority = (index: number): number => { return 1000 - index; }; @@ -48,37 +51,33 @@ export const configureSetIndicator = ( button.command = COMMANDS.SWITCH_BUTTON_SET; }; -export class StatusBarManager { - private buttonSetManager: ButtonSetManager | null = null; +export class StatusBarManager implements vscode.Disposable { + private readonly configReader: ConfigReader; + private readonly statusBarCreator: StatusBarCreator; private statusBarItems: vscode.StatusBarItem[] = []; - private readonly unsubscribers: Array<() => void> = []; - - constructor( - private configReader: ConfigReader, - private statusBarCreator: StatusBarCreator, - private configManager: ConfigManager, - private eventBus?: EventBus - ) { - this.setupEventListeners(); + private readonly store: AppStoreInstance; + private storeUnsubscribe?: () => void; + + private constructor(deps: { + configReader: ConfigReader; + statusBarCreator: StatusBarCreator; + store?: AppStoreInstance; + }) { + this.configReader = deps.configReader; + this.statusBarCreator = deps.statusBarCreator; + this.store = deps.store ?? getAppStore(); + this.setupStoreSubscription(); } - static create = ({ - configManager, - configReader, - eventBus, - statusBarCreator, - }: { - configManager: ConfigManager; + static create = (deps: { configReader: ConfigReader; - eventBus?: EventBus; statusBarCreator: StatusBarCreator; - }): StatusBarManager => - new StatusBarManager(configReader, statusBarCreator, configManager, eventBus); + store?: AppStoreInstance; + }): StatusBarManager => new StatusBarManager(deps); dispose = () => { this.disposeStatusBarItems(); - this.unsubscribers.forEach((unsubscribe) => unsubscribe()); - this.unsubscribers.length = 0; + this.storeUnsubscribe?.(); }; refreshButtons = () => { @@ -88,14 +87,8 @@ export class StatusBarManager { this.createCommandButtons(); }; - setButtonSetManager = (manager: ButtonSetManager) => { - this.buttonSetManager = manager; - }; - private createCommandButtons = () => { - const activeSetButtons = this.buttonSetManager?.getButtonsForActiveSet(); - const buttons = - activeSetButtons ?? this.configManager.getButtonsWithFallback(this.configReader).buttons; + const buttons = this.store.getState().buttons; buttons.forEach((button, index) => { const statusBarItem = this.statusBarCreator( @@ -131,14 +124,12 @@ export class StatusBarManager { }; private createSetIndicator = () => { - if (!this.buttonSetManager) return; - const setIndicator = this.statusBarCreator( vscode.StatusBarAlignment.Left, SET_INDICATOR_PRIORITY ); - const activeSet = this.buttonSetManager.getActiveSet(); + const activeSet = this.store.getState().activeSet; configureSetIndicator(setIndicator, activeSet); setIndicator.show(); @@ -150,19 +141,17 @@ export class StatusBarManager { this.statusBarItems = []; }; - private setupEventListeners = () => { - if (!this.eventBus) { - return; - } - - const refresh = () => this.refreshButtons(); - - 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 = () => { + this.storeUnsubscribe = this.store.subscribe( + (state) => ({ activeSet: state.activeSet, buttons: state.buttons }), + () => { + try { + this.refreshButtons(); + } catch (error) { + console.error("[StatusBarManager] Failed to refresh buttons:", error); + } + }, + { equalityFn: shallowEqual } ); }; }