A wayfinder inspired map plugin for obisidian
at main 122 lines 3.8 kB view raw
1/** 2 * main.ts — Plugin Entry (Effectful/Obsidian API) 3 * 4 * Registers the map viewer plugin, its custom view, commands, 5 * ribbon icon, and workspace event handlers. 6 */ 7 8import { Plugin, MarkdownView } from "obsidian"; 9import type { WorkspaceLeaf, TAbstractFile, TFile } from "obsidian"; 10import { VIEW_TYPE, MapViewerView } from "./mapView"; 11 12export default class MapViewerPlugin extends Plugin { 13 private lastActiveFilePath: string | null = null; 14 15 async onload(): Promise<void> { 16 // Contract #1: Register view type with factory 17 this.registerView(VIEW_TYPE, (leaf: WorkspaceLeaf) => { 18 return new MapViewerView(leaf); 19 }); 20 21 // Contract #2: Ribbon icon 22 this.addRibbonIcon("map-pin", "Open map view", () => { 23 this.activateView(); 24 }); 25 26 // Contract #3: Command 27 this.addCommand({ 28 id: "open-map-view", 29 name: "Open map view", 30 callback: () => { 31 this.activateView(); 32 }, 33 }); 34 35 // Contract #5: active-leaf-change event 36 // NOTE: mapView.ts also registers this event for view-specific logic (e.g., 37 // clearMap on non-MarkdownView). Both handlers may fire; the view's debounce 38 // in scheduleRefresh() collapses duplicate triggers. main.ts adds 39 // lastActiveFilePath dedup to avoid unnecessary refresh calls. 40 this.registerEvent( 41 this.app.workspace.on("active-leaf-change", (leaf: WorkspaceLeaf | null) => { 42 this.onActiveLeafChange(leaf); 43 }) 44 ); 45 46 // Contract #6: vault modify event 47 // NOTE: mapView.ts also registers this event with its own write-guard check. 48 // Both handlers may fire; the view's debounce collapses duplicate triggers. 49 this.registerEvent( 50 this.app.vault.on("modify", (file: TAbstractFile) => { 51 this.onFileModify(file as TFile); 52 }) 53 ); 54 } 55 56 onunload(): void { 57 // Contract #7: Obsidian handles view deregistration automatically. 58 // No special cleanup needed. 59 } 60 61 /** 62 * Contract #4: Open the map view in the right sidebar as a singleton. 63 */ 64 private async activateView(): Promise<void> { 65 const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE); 66 67 if (leaves.length > 0) { 68 // Reveal existing leaf 69 this.app.workspace.revealLeaf(leaves[0]); 70 return; 71 } 72 73 // Create new leaf in right sidebar 74 const leaf = this.app.workspace.getRightLeaf(false); 75 if (!leaf) return; 76 await leaf.setViewState({ 77 type: VIEW_TYPE, 78 active: true, 79 }); 80 this.app.workspace.revealLeaf(leaf); 81 } 82 83 /** 84 * Contract #5: When active leaf changes, call the view's refresh() 85 * if the new leaf is a MarkdownView and the file has changed. 86 */ 87 private onActiveLeafChange(_leaf: WorkspaceLeaf | null): void { 88 const mdView = this.app.workspace.getActiveViewOfType(MarkdownView); 89 if (!mdView) return; 90 91 // Dedup: skip if same file as last time 92 const filePath = mdView.file?.path ?? null; 93 if (filePath !== null && filePath === this.lastActiveFilePath) return; 94 this.lastActiveFilePath = filePath; 95 96 this.refreshMapView(); 97 } 98 99 /** 100 * Contract #6: When a file is modified, refresh the map view 101 * if the modified file is the currently active file. 102 */ 103 private onFileModify(file: TFile): void { 104 const mdView = this.app.workspace.getActiveViewOfType(MarkdownView); 105 if (!mdView?.file || mdView.file !== file) return; 106 107 this.refreshMapView(); 108 } 109 110 /** 111 * Find the map-viewer view and call its refresh() method. 112 */ 113 private refreshMapView(): void { 114 const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE); 115 if (leaves.length === 0) return; 116 117 const view = leaves[0].view; 118 if (view && typeof (view as MapViewerView).refresh === "function") { 119 (view as MapViewerView).refresh(); 120 } 121 } 122}