/** * main.ts — Plugin Entry (Effectful/Obsidian API) * * Registers the map viewer plugin, its custom view, commands, * ribbon icon, and workspace event handlers. */ import { Plugin, MarkdownView } from "obsidian"; import type { WorkspaceLeaf, TAbstractFile, TFile } from "obsidian"; import { VIEW_TYPE, MapViewerView } from "./mapView"; export default class MapViewerPlugin extends Plugin { private lastActiveFilePath: string | null = null; async onload(): Promise { // Contract #1: Register view type with factory this.registerView(VIEW_TYPE, (leaf: WorkspaceLeaf) => { return new MapViewerView(leaf); }); // Contract #2: Ribbon icon this.addRibbonIcon("map-pin", "Open map view", () => { this.activateView(); }); // Contract #3: Command this.addCommand({ id: "open-map-view", name: "Open map view", callback: () => { this.activateView(); }, }); // Contract #5: active-leaf-change event // NOTE: mapView.ts also registers this event for view-specific logic (e.g., // clearMap on non-MarkdownView). Both handlers may fire; the view's debounce // in scheduleRefresh() collapses duplicate triggers. main.ts adds // lastActiveFilePath dedup to avoid unnecessary refresh calls. this.registerEvent( this.app.workspace.on("active-leaf-change", (leaf: WorkspaceLeaf | null) => { this.onActiveLeafChange(leaf); }) ); // Contract #6: vault modify event // NOTE: mapView.ts also registers this event with its own write-guard check. // Both handlers may fire; the view's debounce collapses duplicate triggers. this.registerEvent( this.app.vault.on("modify", (file: TAbstractFile) => { this.onFileModify(file as TFile); }) ); } onunload(): void { // Contract #7: Obsidian handles view deregistration automatically. // No special cleanup needed. } /** * Contract #4: Open the map view in the right sidebar as a singleton. */ private async activateView(): Promise { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE); if (leaves.length > 0) { // Reveal existing leaf this.app.workspace.revealLeaf(leaves[0]); return; } // Create new leaf in right sidebar const leaf = this.app.workspace.getRightLeaf(false); if (!leaf) return; await leaf.setViewState({ type: VIEW_TYPE, active: true, }); this.app.workspace.revealLeaf(leaf); } /** * Contract #5: When active leaf changes, call the view's refresh() * if the new leaf is a MarkdownView and the file has changed. */ private onActiveLeafChange(_leaf: WorkspaceLeaf | null): void { const mdView = this.app.workspace.getActiveViewOfType(MarkdownView); if (!mdView) return; // Dedup: skip if same file as last time const filePath = mdView.file?.path ?? null; if (filePath !== null && filePath === this.lastActiveFilePath) return; this.lastActiveFilePath = filePath; this.refreshMapView(); } /** * Contract #6: When a file is modified, refresh the map view * if the modified file is the currently active file. */ private onFileModify(file: TFile): void { const mdView = this.app.workspace.getActiveViewOfType(MarkdownView); if (!mdView?.file || mdView.file !== file) return; this.refreshMapView(); } /** * Find the map-viewer view and call its refresh() method. */ private refreshMapView(): void { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE); if (leaves.length === 0) return; const view = leaves[0].view; if (view && typeof (view as MapViewerView).refresh === "function") { (view as MapViewerView).refresh(); } } }