@@ -5,16 +5,24 @@ local NotificationPopup = require('orgmode.notifications.notification_popup')
55local current_file_path = string.sub (debug.getinfo (1 , ' S' ).source , 2 )
66local root_path = vim .fn .fnamemodify (current_file_path , ' :p:h:h:h:h' )
77
8+ --- @class OrgNotificationsCacheEntry
9+ --- @field mtime number
10+ --- @field mtime_sec number
11+ --- @field changedtick number
12+ --- @field headlines { headline : OrgHeadline , dates : OrgDate[] } []
13+
814--- @class OrgNotifications
915--- @field timer table
1016--- @field files OrgFiles
17+ --- @field _file_cache table<string , OrgNotificationsCacheEntry>
1118local Notifications = {}
1219
1320--- @param opts { files : OrgFiles }
1421function Notifications :new (opts )
1522 local data = {
1623 timer = nil ,
1724 files = opts .files ,
25+ _file_cache = {},
1826 }
1927 setmetatable (data , self )
2028 self .__index = self
@@ -41,36 +49,62 @@ function Notifications:stop_timer()
4149 end
4250end
4351
52+ --- @private
53+ --- Run get_tasks as a coroutine, processing file batches across event loop iterations
4454--- @param time OrgDate
45- function Notifications :notify (time )
46- local tasks = self :get_tasks (time )
55+ --- @param callback fun ( tasks : table[] )
56+ function Notifications :_get_tasks_async (time , callback )
57+ local co = coroutine.create (function ()
58+ return self :get_tasks (time )
59+ end )
4760
48- if type (config .notifications .notifier ) == ' function' then
49- return config .notifications .notifier (tasks )
61+ local function step ()
62+ local ok , result = coroutine.resume (co )
63+ if not ok then
64+ vim .notify (' [orgmode] notification error: ' .. tostring (result ), vim .log .levels .ERROR )
65+ return
66+ end
67+ if coroutine.status (co ) == ' dead' then
68+ callback (result )
69+ else
70+ vim .schedule (step )
71+ end
5072 end
5173
52- local result = {}
53- for _ , task in ipairs (tasks ) do
54- utils .concat (result , {
55- string.format (' # %s (%s)' , task .category , task .humanized_duration ),
56- string.format (' %s %s %s' , string.rep (' *' , task .level ), task .todo or ' ' , task .title ),
57- string.format (' %s: <%s>' , task .type , task .time :to_string ()),
58- })
59- end
74+ step ()
75+ end
6076
61- if not vim .tbl_isempty (result ) then
62- NotificationPopup :new ({ content = result , border = config .win_border })
63- end
77+ --- @param time OrgDate
78+ function Notifications :notify (time )
79+ self :_get_tasks_async (time , function (tasks )
80+ if type (config .notifications .notifier ) == ' function' then
81+ return config .notifications .notifier (tasks )
82+ end
83+
84+ local result = {}
85+ for _ , task in ipairs (tasks ) do
86+ utils .concat (result , {
87+ string.format (' # %s (%s)' , task .category , task .humanized_duration ),
88+ string.format (' %s %s %s' , string.rep (' *' , task .level ), task .todo or ' ' , task .title ),
89+ string.format (' %s: <%s>' , task .type , task .time :to_string ()),
90+ })
91+ end
92+
93+ if not vim .tbl_isempty (result ) then
94+ NotificationPopup :new ({ content = result , border = config .win_border })
95+ end
96+ end )
6497end
6598
6699function Notifications :cron ()
67- local tasks = self :get_tasks (Date .now ())
68- if type (config .notifications .cron_notifier ) == ' function' then
69- config .notifications .cron_notifier (tasks )
70- else
71- self :_cron_notifier (tasks )
72- end
73- vim .cmd ([[ qall!]] )
100+ self :_get_tasks_async (Date .now (), function (tasks )
101+ if type (config .notifications .cron_notifier ) == ' function' then
102+ config .notifications .cron_notifier (tasks )
103+ else
104+ self :_cron_notifier (tasks )
105+ end
106+ vim .cmd ([[ qall!]] )
107+ end )
74108end
75109
76110--- @param tasks table[]
@@ -96,33 +130,97 @@ function Notifications:_cron_notifier(tasks)
96130 end
97131end
98132
133+ --- @private
134+ --- Check if the cache entry for a file is still valid
135+ --- @param orgfile OrgFile
136+ --- @return boolean
137+ function Notifications :_is_cache_valid (orgfile )
138+ local cached = self ._file_cache [orgfile .filename ]
139+ if not cached then
140+ return false
141+ end
142+ return cached .mtime == orgfile .metadata .mtime
143+ and cached .mtime_sec == orgfile .metadata .mtime_sec
144+ and cached .changedtick == orgfile .metadata .changedtick
145+ end
146+
147+ --- @private
148+ --- Get cached headline data for a file, rebuilding the cache if the file has changed
149+ --- @param orgfile OrgFile
150+ --- @return { headline : OrgHeadline , dates : OrgDate[] } []
151+ function Notifications :_get_cached_headlines (orgfile )
152+ if self :_is_cache_valid (orgfile ) then
153+ return self ._file_cache [orgfile .filename ].headlines
154+ end
155+
156+ local headline_data = {}
157+ for _ , headline in ipairs (orgfile :get_opened_unfinished_headlines ()) do
158+ local dates = headline :get_deadline_and_scheduled_dates ()
159+ if # dates > 0 then
160+ table.insert (headline_data , { headline = headline , dates = dates })
161+ end
162+ end
163+
164+ self ._file_cache [orgfile .filename ] = {
165+ mtime = orgfile .metadata .mtime ,
166+ mtime_sec = orgfile .metadata .mtime_sec ,
167+ changedtick = orgfile .metadata .changedtick ,
168+ headlines = headline_data ,
169+ }
170+
171+ return headline_data
172+ end
173+
99174--- @param time OrgDate
100175function Notifications :get_tasks (time )
101176 local tasks = {}
102- for _ , orgfile in ipairs (self .files :all ()) do
103- for _ , headline in ipairs (orgfile :get_opened_unfinished_headlines ()) do
104- for _ , date in ipairs (headline :get_deadline_and_scheduled_dates ()) do
105- local reminders = self :_check_reminders (date , time )
106- for _ , reminder in ipairs (reminders ) do
107- table.insert (tasks , {
108- file = orgfile .filename ,
109- todo = headline :get_todo (),
110- category = headline :get_category (),
111- priority = headline :get_priority (),
112- title = headline :get_title (),
113- level = headline :get_level (),
114- tags = headline :get_tags (),
115- original_time = date ,
116- time = reminder .time ,
117- reminder_type = reminder .reminder_type ,
118- minutes = reminder .minutes ,
119- humanized_duration = utils .humanize_minutes (reminder .minutes ),
120- type = date .type ,
121- range = headline :get_range (),
122- })
177+ local orgfiles = self .files :all ()
178+ local file_idx = 1
179+ local batch_size = 3
180+
181+ local function process_batch ()
182+ local batch_end = math.min (file_idx + batch_size - 1 , # orgfiles )
183+ for i = file_idx , batch_end do
184+ local orgfile = orgfiles [i ]
185+ local headline_data = self :_get_cached_headlines (orgfile )
186+ for _ , entry in ipairs (headline_data ) do
187+ for _ , date in ipairs (entry .dates ) do
188+ local reminders = self :_check_reminders (date , time )
189+ -- only build task objects when reminders match
190+ if # reminders > 0 then
191+ local headline = entry .headline
192+ for _ , reminder in ipairs (reminders ) do
193+ table.insert (tasks , {
194+ file = orgfile .filename ,
195+ todo = headline :get_todo (),
196+ category = headline :get_category (),
197+ priority = headline :get_priority (),
198+ title = headline :get_title (),
199+ level = headline :get_level (),
200+ tags = headline :get_tags (),
201+ original_time = date ,
202+ time = reminder .time ,
203+ reminder_type = reminder .reminder_type ,
204+ minutes = reminder .minutes ,
205+ humanized_duration = utils .humanize_minutes (reminder .minutes ),
206+ type = date .type ,
207+ range = headline :get_range (),
208+ })
209+ end
210+ end
123211 end
124212 end
125213 end
214+ file_idx = batch_end + 1
215+ end
216+
217+ -- process files in batches, yielding between batches to avoid blocking the editor
218+ local _ , is_main = coroutine.running ()
219+ while file_idx <= # orgfiles do
220+ process_batch ()
221+ if not is_main and file_idx <= # orgfiles then
222+ coroutine.yield ()
223+ end
126224 end
127225
128226 return tasks
0 commit comments