1+ import { App , EventRef , TFile } from "obsidian" ;
2+
3+
4+ export class BacklinkIndex {
5+ // 核心索引数据结构
6+ backlinkMap : Map < string , Set < string > > = new Map ( ) ;
7+ forwardMap : Map < string , Set < string > > = new Map ( ) ;
8+ app : App ;
9+ isInitialized : boolean = false ;
10+ fileWatcherRefs : EventRef [ ] = [ ] ; // 存储文件监听器的 EventRef 以便注销
11+
12+ constructor ( app : App ) {
13+ this . app = app ;
14+ }
15+
16+ // 全量初始化索引
17+ public init ( ) {
18+ if ( this . isInitialized ) return ;
19+ this . backlinkMap . clear ( ) ;
20+ this . forwardMap . clear ( ) ;
21+ this . isInitialized = true ;
22+
23+ const resolvedLinks = this . app . metadataCache . resolvedLinks ;
24+ for ( const sourcePath in resolvedLinks ) {
25+ const targets = Object . keys ( resolvedLinks [ sourcePath ] || { } ) ;
26+ for ( const targetPath of targets ) {
27+ this . addLink ( sourcePath , targetPath ) ;
28+ }
29+ }
30+
31+ const resolveRef = this . app . metadataCache . on ( 'resolve' , ( file ) => {
32+ if ( file instanceof TFile && file . path . endsWith ( ".md" ) ) {
33+ this . updateFileIndex ( file . path ) ;
34+ }
35+ } ) ;
36+ const renameRef = this . app . vault . on ( "rename" , ( file , oldPath ) => {
37+ if ( file instanceof TFile && file . path . endsWith ( ".md" ) ) {
38+ this . removeSourceFromIndex ( oldPath ) ;
39+ this . updateFileIndex ( file . path ) ;
40+ }
41+ } ) ;
42+ const deleteRef = this . app . vault . on ( "delete" , ( file ) => {
43+ if ( file instanceof TFile && file . path . endsWith ( ".md" ) ) {
44+ this . removeSourceFromIndex ( file . path ) ;
45+ // 同时也确保作为 Target 的索引被抹除
46+ this [ 'backlinkMap' ] . delete ( file . path ) ;
47+ }
48+ } ) ;
49+ this . fileWatcherRefs . push ( resolveRef ) ;
50+ this . fileWatcherRefs . push ( renameRef ) ;
51+ this . fileWatcherRefs . push ( deleteRef ) ;
52+ }
53+
54+ // 清理索引和监听器
55+ public close ( ) {
56+ this . backlinkMap . clear ( ) ;
57+ this . forwardMap . clear ( ) ;
58+ this . isInitialized = false ;
59+ this . fileWatcherRefs . forEach ( ref => this . app . workspace . offref ( ref ) ) ;
60+ this . fileWatcherRefs = [ ] ;
61+ }
62+
63+ // 更新单个文件的索引(增量更新)
64+ public updateFileIndex ( sourcePath : string ) {
65+ // 1. 先移除该文件之前所有的旧链接关系
66+ this . removeSourceFromIndex ( sourcePath ) ;
67+
68+ // 2. 获取 MetadataCache 中最新的链接数据并重新添加
69+ const newLinks = this . app . metadataCache . resolvedLinks [ sourcePath ] ;
70+ if ( newLinks ) {
71+ for ( const targetPath in newLinks ) {
72+ this . addLink ( sourcePath , targetPath ) ;
73+ }
74+ }
75+ }
76+
77+ // 获取反向链接列表
78+ public getBacklinks ( targetPath : string ) : string [ ] {
79+ const sources = this . backlinkMap . get ( targetPath ) ;
80+ return sources ? Array . from ( sources ) : [ ] ;
81+ }
82+
83+ // 建立单向连接关系
84+ private addLink ( source : string , target : string ) {
85+ // 更新反向索引
86+ if ( ! this . backlinkMap . has ( target ) ) this . backlinkMap . set ( target , new Set ( ) ) ;
87+ this . backlinkMap . get ( target ) ! . add ( source ) ;
88+
89+ // 更新正向索引(用于辅助增量更新)
90+ if ( ! this . forwardMap . has ( source ) ) this . forwardMap . set ( source , new Set ( ) ) ;
91+ this . forwardMap . get ( source ) ! . add ( target ) ;
92+ }
93+
94+ // 移除一个源文件的所有索引轨迹
95+ public removeSourceFromIndex ( sourcePath : string ) {
96+ const targets = this . forwardMap . get ( sourcePath ) ;
97+ if ( ! targets ) return ;
98+
99+ // 遍历该源文件之前指向的所有目标,从它们的 Backlink 集合中删掉自己
100+ for ( const targetPath of targets ) {
101+ const backlinkSet = this . backlinkMap . get ( targetPath ) ;
102+ if ( backlinkSet ) {
103+ backlinkSet . delete ( sourcePath ) ;
104+ // 如果该目标已经没有任何反向链接,清理 Map 空间
105+ if ( backlinkSet . size === 0 ) this . backlinkMap . delete ( targetPath ) ;
106+ }
107+ }
108+
109+ // 最后清理正向索引
110+ this . forwardMap . delete ( sourcePath ) ;
111+ }
112+ }
0 commit comments