@@ -12,10 +12,10 @@ use crate::common::json_watch::{watch_json, JsonWatcher};
1212use crate :: common:: lru:: LruCache ;
1313use crate :: common:: query:: parse_query_filters;
1414use crate :: linking:: {
15- build_index_from_notes_and_todos, format_link_id, EntityKey , LinkRef , LinkTarget ,
15+ build_index_from_notes_and_todos, format_link_id, EntityKey , LinkIndex , LinkRef , LinkTarget ,
1616} ;
1717use crate :: plugin:: Plugin ;
18- use crate :: plugins:: note:: load_notes;
18+ use crate :: plugins:: note:: { load_notes, note_version , Note } ;
1919use base64:: engine:: general_purpose:: URL_SAFE_NO_PAD ;
2020use base64:: Engine ;
2121use fuzzy_matcher:: skim:: SkimMatcherV2 ;
@@ -206,12 +206,70 @@ pub static TODO_DATA: Lazy<Arc<RwLock<Vec<TodoEntry>>>> =
206206static TODO_CACHE : Lazy < Arc < RwLock < LruCache < String , Vec < Action > > > > > =
207207 Lazy :: new ( || Arc :: new ( RwLock :: new ( LruCache :: new ( 64 ) ) ) ) ;
208208
209+ #[ derive( Clone ) ]
210+ struct TodoLinksIndexCache {
211+ todo_version : u64 ,
212+ note_version : u64 ,
213+ notes : Vec < Note > ,
214+ index : LinkIndex ,
215+ }
216+
217+ static TODO_LINKS_INDEX_CACHE : Lazy < RwLock < Option < TodoLinksIndexCache > > > =
218+ Lazy :: new ( || RwLock :: new ( None ) ) ;
219+
220+ static TODO_LINKS_INDEX_REBUILD_COUNT : AtomicU64 = AtomicU64 :: new ( 0 ) ;
221+
209222fn invalidate_todo_cache ( ) {
210223 if let Ok ( mut cache) = TODO_CACHE . write ( ) {
211224 cache. clear ( ) ;
212225 }
213226}
214227
228+ fn invalidate_todo_links_index_cache ( ) {
229+ if let Ok ( mut cache) = TODO_LINKS_INDEX_CACHE . write ( ) {
230+ * cache = None ;
231+ }
232+ }
233+
234+ fn get_todo_links_index ( notes_todos : & [ TodoEntry ] ) -> ( Vec < Note > , LinkIndex ) {
235+ let todo_ver = todo_version ( ) ;
236+ let note_ver = note_version ( ) ;
237+ if let Ok ( cache) = TODO_LINKS_INDEX_CACHE . read ( ) {
238+ if let Some ( entry) = cache. as_ref ( ) {
239+ if entry. todo_version == todo_ver && entry. note_version == note_ver {
240+ return ( entry. notes . clone ( ) , entry. index . clone ( ) ) ;
241+ }
242+ }
243+ }
244+
245+ let notes = load_notes ( ) . unwrap_or_default ( ) ;
246+ let todos = notes_todos. to_vec ( ) ;
247+ let index = build_index_from_notes_and_todos ( & notes, & todos) ;
248+ TODO_LINKS_INDEX_REBUILD_COUNT . fetch_add ( 1 , Ordering :: SeqCst ) ;
249+
250+ if let Ok ( mut cache) = TODO_LINKS_INDEX_CACHE . write ( ) {
251+ * cache = Some ( TodoLinksIndexCache {
252+ todo_version : todo_ver,
253+ note_version : note_ver,
254+ notes : notes. clone ( ) ,
255+ index : index. clone ( ) ,
256+ } ) ;
257+ }
258+
259+ ( notes, index)
260+ }
261+
262+ #[ cfg( test) ]
263+ fn reset_todo_links_index_cache_state ( ) {
264+ invalidate_todo_links_index_cache ( ) ;
265+ TODO_LINKS_INDEX_REBUILD_COUNT . store ( 0 , Ordering :: SeqCst ) ;
266+ }
267+
268+ #[ cfg( test) ]
269+ fn todo_links_index_rebuild_count ( ) -> u64 {
270+ TODO_LINKS_INDEX_REBUILD_COUNT . load ( Ordering :: SeqCst )
271+ }
272+
215273fn bump_todo_version ( ) {
216274 TODO_VERSION . fetch_add ( 1 , Ordering :: SeqCst ) ;
217275}
@@ -257,6 +315,7 @@ fn update_cache(list: Vec<TodoEntry>) {
257315 * lock = list;
258316 }
259317 invalidate_todo_cache ( ) ;
318+ invalidate_todo_links_index_cache ( ) ;
260319 bump_todo_version ( ) ;
261320}
262321
@@ -364,6 +423,7 @@ impl TodoPlugin {
364423 if let Ok ( mut c) = cache_clone. write ( ) {
365424 c. clear ( ) ;
366425 }
426+ invalidate_todo_links_index_cache ( ) ;
367427 bump_todo_version ( ) ;
368428 }
369429 }
@@ -768,9 +828,8 @@ impl TodoPlugin {
768828 let Some ( todo) = matches. first ( ) else {
769829 return Vec :: new ( ) ;
770830 } ;
771- let notes = load_notes ( ) . unwrap_or_default ( ) ;
772831 let todos = guard. clone ( ) ;
773- let index = build_index_from_notes_and_todos ( & notes , & todos) ;
832+ let ( notes , index) = get_todo_links_index ( & todos) ;
774833 let source = EntityKey :: new ( LinkTarget :: Todo , todo. id . clone ( ) ) ;
775834 let mut actions: Vec < Action > = index
776835 . get_forward_links ( & source)
@@ -1330,6 +1389,52 @@ mod tests {
13301389 }
13311390 }
13321391
1392+ #[ test]
1393+ fn todo_links_reuses_cached_index_between_queries ( ) {
1394+ reset_todo_links_index_cache_state ( ) ;
1395+ let original = set_todos ( vec ! [ TodoEntry {
1396+ id: "t-cache" . into( ) ,
1397+ text: "cache me" . into( ) ,
1398+ done: false ,
1399+ priority: 1 ,
1400+ tags: vec![ ] ,
1401+ entity_refs: vec![ EntityRef :: new( EntityKind :: Note , "alpha" , None ) ] ,
1402+ } ] ) ;
1403+
1404+ let plugin = TodoPlugin {
1405+ matcher : SkimMatcherV2 :: default ( ) ,
1406+ data : TODO_DATA . clone ( ) ,
1407+ cache : TODO_CACHE . clone ( ) ,
1408+ watcher : None ,
1409+ } ;
1410+
1411+ let first = plugin. search_internal ( "todo links id:t-cache" ) ;
1412+ assert ! ( !first. is_empty( ) ) ;
1413+ assert_eq ! ( todo_links_index_rebuild_count( ) , 1 ) ;
1414+
1415+ let second = plugin. search_internal ( "todo links id:t-cache" ) ;
1416+ assert ! ( !second. is_empty( ) ) ;
1417+ assert_eq ! ( todo_links_index_rebuild_count( ) , 1 ) ;
1418+
1419+ update_cache ( vec ! [ TodoEntry {
1420+ id: "t-cache" . into( ) ,
1421+ text: "cache me updated" . into( ) ,
1422+ done: false ,
1423+ priority: 1 ,
1424+ tags: vec![ ] ,
1425+ entity_refs: vec![ EntityRef :: new( EntityKind :: Note , "alpha" , None ) ] ,
1426+ } ] ) ;
1427+
1428+ let third = plugin. search_internal ( "todo links id:t-cache" ) ;
1429+ assert ! ( !third. is_empty( ) ) ;
1430+ assert_eq ! ( todo_links_index_rebuild_count( ) , 2 ) ;
1431+
1432+ if let Ok ( mut guard) = TODO_DATA . write ( ) {
1433+ * guard = original;
1434+ }
1435+ reset_todo_links_index_cache_state ( ) ;
1436+ }
1437+
13331438 #[ test]
13341439 fn todo_links_json_output_prefixes_machine_readable_row ( ) {
13351440 let original = set_todos ( vec ! [ TodoEntry {
0 commit comments