@@ -3,10 +3,10 @@ use crate::common::slug::slugify;
33use crate :: gui:: LauncherApp ;
44use crate :: plugin:: Plugin ;
55use crate :: plugins:: note:: {
6- assets_dir, available_tags, image_files, note_cache_snapshot, resolve_note_query , save_note ,
7- Note , NoteExternalOpen , NotePlugin , NoteTarget ,
6+ assets_dir, available_tags, image_files, note_cache_snapshot, note_version , resolve_note_query ,
7+ save_note , Note , NoteExternalOpen , NotePlugin , NoteTarget ,
88} ;
9- use crate :: plugins:: todo:: { load_todos, TODO_FILE } ;
9+ use crate :: plugins:: todo:: { load_todos, todo_version , TODO_FILE } ;
1010use eframe:: egui:: { self , popup, Color32 , FontId , Key } ;
1111use egui_commonmark:: { CommonMarkCache , CommonMarkViewer } ;
1212use egui_toast:: { Toast , ToastKind , ToastOptions } ;
@@ -23,10 +23,12 @@ use std::process::Command;
2323use std:: {
2424 env,
2525 path:: { Path , PathBuf } ,
26+ time:: Duration ,
2627} ;
2728use url:: Url ;
2829
2930const BACKLINK_PAGE_SIZE : usize = 12 ;
31+ const HEAVY_RECOMPUTE_IDLE_DEBOUNCE : Duration = Duration :: from_millis ( 250 ) ;
3032
3133#[ derive( Clone , Copy , PartialEq , Eq ) ]
3234enum BacklinkTab {
@@ -174,9 +176,13 @@ pub struct NotePanel {
174176 focus_textedit_next_frame : bool ,
175177 last_textedit_id : Option < egui:: Id > ,
176178 derived : NoteDerivedView ,
177- derived_dirty : bool ,
179+ fast_derived_dirty : bool ,
180+ heavy_recompute_requested : bool ,
181+ last_edit_at_secs : Option < f64 > ,
182+ last_notes_version : u64 ,
183+ last_todo_revision : u64 ,
178184 #[ cfg( test) ]
179- derived_recompute_count : usize ,
185+ heavy_recompute_count : usize ,
180186}
181187
182188#[ derive( Default , Clone ) ]
@@ -214,11 +220,16 @@ impl NotePanel {
214220 focus_textedit_next_frame : false ,
215221 last_textedit_id : None ,
216222 derived : NoteDerivedView :: default ( ) ,
217- derived_dirty : true ,
223+ fast_derived_dirty : true ,
224+ heavy_recompute_requested : true ,
225+ last_edit_at_secs : None ,
226+ last_notes_version : 0 ,
227+ last_todo_revision : 0 ,
218228 #[ cfg( test) ]
219- derived_recompute_count : 0 ,
229+ heavy_recompute_count : 0 ,
220230 } ;
221- panel. refresh_derived ( ) ;
231+ panel. refresh_fast_derived ( ) ;
232+ panel. refresh_heavy_derived ( true ) ;
222233 panel
223234 }
224235
@@ -230,46 +241,71 @@ impl NotePanel {
230241 }
231242 }
232243
233- fn refresh_derived ( & mut self ) {
244+ fn refresh_fast_derived ( & mut self ) {
245+ self . derived . tags = extract_tags ( & self . note . content ) ;
246+ self . derived . wiki_links = extract_wiki_links ( & self . note . content )
247+ . into_iter ( )
248+ . filter ( |l| slugify ( l) != self . note . slug )
249+ . collect ( ) ;
250+ self . derived . external_links = extract_links ( & self . note . content ) ;
251+ self . fast_derived_dirty = false ;
252+ }
253+
254+ fn refresh_heavy_derived ( & mut self , force : bool ) {
255+ let current_notes_version = note_version ( ) ;
256+ let current_todo_revision = todo_version ( ) ;
257+ if !force
258+ && self . last_notes_version == current_notes_version
259+ && self . last_todo_revision == current_todo_revision
260+ {
261+ self . heavy_recompute_requested = false ;
262+ return ;
263+ }
264+
234265 let todos = load_todos ( TODO_FILE ) . unwrap_or_default ( ) ;
235- let todo_label_map = todos
266+ self . derived . todo_label_map = todos
236267 . iter ( )
237268 . filter ( |t| !t. id . is_empty ( ) )
238269 . map ( |t| ( t. id . clone ( ) , t. text . clone ( ) ) )
239270 . collect :: < HashMap < _ , _ > > ( ) ;
240- let notes = note_cache_snapshot ( ) ;
241271
242- self . derived = NoteDerivedView {
243- tags : extract_tags ( & self . note . content ) ,
244- wiki_links : extract_wiki_links ( & self . note . content )
245- . into_iter ( )
246- . filter ( |l| slugify ( l) != self . note . slug )
247- . collect ( ) ,
248- external_links : extract_links ( & self . note . content ) ,
249- backlink_rows_linked_todos : backlink_rows_for_note (
250- & self . note . slug ,
251- BacklinkTab :: LinkedTodos ,
252- & todos,
253- & notes,
254- ) ,
255- backlink_rows_related_notes : backlink_rows_for_note (
256- & self . note . slug ,
257- BacklinkTab :: RelatedNotes ,
258- & todos,
259- & notes,
260- ) ,
261- backlink_rows_mentions : backlink_rows_for_note (
262- & self . note . slug ,
263- BacklinkTab :: Mentions ,
264- & todos,
265- & notes,
266- ) ,
267- todo_label_map,
268- } ;
269- self . derived_dirty = false ;
272+ let notes = note_cache_snapshot ( ) ;
273+ self . derived . backlink_rows_linked_todos =
274+ backlink_rows_for_note ( & self . note . slug , BacklinkTab :: LinkedTodos , & todos, & notes) ;
275+ self . derived . backlink_rows_related_notes =
276+ backlink_rows_for_note ( & self . note . slug , BacklinkTab :: RelatedNotes , & todos, & notes) ;
277+ self . derived . backlink_rows_mentions =
278+ backlink_rows_for_note ( & self . note . slug , BacklinkTab :: Mentions , & todos, & notes) ;
279+
280+ self . last_notes_version = current_notes_version;
281+ self . last_todo_revision = current_todo_revision;
282+ self . heavy_recompute_requested = false ;
270283 #[ cfg( test) ]
271284 {
272- self . derived_recompute_count += 1 ;
285+ self . heavy_recompute_count += 1 ;
286+ }
287+ }
288+
289+ fn mark_content_changed ( & mut self , now_secs : f64 ) {
290+ self . fast_derived_dirty = true ;
291+ self . heavy_recompute_requested = true ;
292+ self . last_edit_at_secs = Some ( now_secs) ;
293+ }
294+
295+ fn maybe_refresh_heavy_derived ( & mut self , ctx : & egui:: Context ) {
296+ let notes_changed = self . last_notes_version != note_version ( ) ;
297+ let todos_changed = self . last_todo_revision != todo_version ( ) ;
298+ let debounce_elapsed = self
299+ . last_edit_at_secs
300+ . map ( |t| ctx. input ( |i| i. time - t) >= HEAVY_RECOMPUTE_IDLE_DEBOUNCE . as_secs_f64 ( ) )
301+ . unwrap_or ( false ) ;
302+ if notes_changed || todos_changed || debounce_elapsed {
303+ self . refresh_heavy_derived ( false ) ;
304+ return ;
305+ }
306+
307+ if self . heavy_recompute_requested {
308+ ctx. request_repaint_after ( HEAVY_RECOMPUTE_IDLE_DEBOUNCE ) ;
273309 }
274310 }
275311
@@ -392,9 +428,10 @@ impl NotePanel {
392428 app. note_font_size += 1.0 ;
393429 }
394430 } ) ;
395- if self . derived_dirty {
396- self . refresh_derived ( ) ;
431+ if self . fast_derived_dirty {
432+ self . refresh_fast_derived ( ) ;
397433 }
434+ self . maybe_refresh_heavy_derived ( ctx) ;
398435 if !self . derived . tags . is_empty ( ) {
399436 let was_focused = self
400437 . last_textedit_id
@@ -667,7 +704,7 @@ impl NotePanel {
667704 }
668705 if modified {
669706 self . markdown_cache . clear_scrollable ( ) ;
670- self . derived_dirty = true ;
707+ self . mark_content_changed ( ctx . input ( |i| i . time ) ) ;
671708 }
672709 None
673710 } else {
@@ -687,7 +724,7 @@ impl NotePanel {
687724 if !self . preview_mode {
688725 if let Some ( resp) = resp. inner {
689726 if resp. changed ( ) {
690- self . derived_dirty = true ;
727+ self . mark_content_changed ( ctx . input ( |i| i . time ) ) ;
691728 }
692729 let first_edit_frame = self . last_textedit_id . is_none ( ) ;
693730 self . last_textedit_id = Some ( resp. id ) ;
@@ -874,7 +911,8 @@ impl NotePanel {
874911 if let Err ( e) = save_note ( & mut self . note , true ) {
875912 app. set_error ( format ! ( "Failed to save note: {e}" ) ) ;
876913 } else {
877- self . refresh_derived ( ) ;
914+ self . refresh_fast_derived ( ) ;
915+ self . refresh_heavy_derived ( true ) ;
878916 self . finish_save ( app) ;
879917 self . overwrite_prompt = false ;
880918 }
@@ -885,7 +923,8 @@ impl NotePanel {
885923 if let Err ( e) = save_note ( & mut self . note , true ) {
886924 app. set_error ( format ! ( "Failed to save note: {e}" ) ) ;
887925 } else {
888- self . refresh_derived ( ) ;
926+ self . refresh_fast_derived ( ) ;
927+ self . refresh_heavy_derived ( true ) ;
889928 self . finish_save ( app) ;
890929 self . overwrite_prompt = false ;
891930 }
@@ -908,15 +947,17 @@ impl NotePanel {
908947 . map ( |l| slugify ( & l) )
909948 . filter ( |l| l != & self . note . slug )
910949 . collect ( ) ;
911- self . derived_dirty = true ;
950+ self . fast_derived_dirty = true ;
951+ self . heavy_recompute_requested = true ;
912952 if let Some ( first) = self . note . content . lines ( ) . next ( ) {
913953 if let Some ( t) = first. strip_prefix ( "# " ) {
914954 self . note . title = t. to_string ( ) ;
915955 }
916956 }
917957 match save_note ( & mut self . note , app. note_always_overwrite ) {
918958 Ok ( true ) => {
919- self . refresh_derived ( ) ;
959+ self . refresh_fast_derived ( ) ;
960+ self . refresh_heavy_derived ( true ) ;
920961 self . finish_save ( app) ;
921962 }
922963 Ok ( false ) => {
@@ -2045,14 +2086,41 @@ Body with [[Other]]"
20452086 entity_refs : Vec :: new ( ) ,
20462087 } ;
20472088 let mut panel = NotePanel :: from_note ( note) ;
2048- let initial = panel. derived_recompute_count ;
2089+ let initial = panel. heavy_recompute_count ;
20492090 let _ = ctx. run ( Default :: default ( ) , |ctx| {
20502091 panel. ui ( ctx, & mut app) ;
20512092 } ) ;
20522093 let _ = ctx. run ( Default :: default ( ) , |ctx| {
20532094 panel. ui ( ctx, & mut app) ;
20542095 } ) ;
2055- assert_eq ! ( panel. derived_recompute_count, initial) ;
2096+ assert_eq ! ( panel. heavy_recompute_count, initial) ;
2097+ }
2098+
2099+ #[ test]
2100+ fn edits_do_not_trigger_heavy_recompute_every_frame ( ) {
2101+ let ctx = egui:: Context :: default ( ) ;
2102+ let mut app = new_app ( & ctx) ;
2103+ let note = Note {
2104+ title : "Title" . into ( ) ,
2105+ path : std:: path:: PathBuf :: new ( ) ,
2106+ content : "# Title\n \n Body" . into ( ) ,
2107+ tags : Vec :: new ( ) ,
2108+ links : Vec :: new ( ) ,
2109+ slug : "title" . into ( ) ,
2110+ alias : None ,
2111+ entity_refs : Vec :: new ( ) ,
2112+ } ;
2113+ let mut panel = NotePanel :: from_note ( note) ;
2114+ let initial = panel. heavy_recompute_count ;
2115+ panel. mark_content_changed ( f64:: MAX ) ;
2116+
2117+ for _ in 0 ..3 {
2118+ let _ = ctx. run ( Default :: default ( ) , |ctx| {
2119+ panel. ui ( ctx, & mut app) ;
2120+ } ) ;
2121+ }
2122+
2123+ assert_eq ! ( panel. heavy_recompute_count, initial) ;
20562124 }
20572125
20582126 #[ test]
@@ -2079,15 +2147,16 @@ Body with [[Other]]"
20792147 entity_refs : Vec :: new ( ) ,
20802148 } ;
20812149 let mut panel = NotePanel :: from_note ( note) ;
2082- let before = panel. derived_recompute_count ;
2150+ let before = panel. heavy_recompute_count ;
20832151 panel. note . content = "# Source
20842152
20852153[[beta]]"
20862154 . into ( ) ;
2087- panel. derived_dirty = true ;
2155+ panel. fast_derived_dirty = true ;
2156+ panel. heavy_recompute_requested = true ;
20882157 panel. save ( & mut app) ;
20892158
2090- assert ! ( panel. derived_recompute_count > before) ;
2159+ assert ! ( panel. heavy_recompute_count > before) ;
20912160 assert_eq ! ( panel. note. links, vec![ "beta" . to_string( ) ] ) ;
20922161
20932162 if let Some ( p) = prev {
0 commit comments