A wayfinder inspired map plugin for obisidian
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}