Line data Source code
1 : -- lua/yoda/timer_manager.lua
2 : -- Centralized timer management to prevent memory leaks
3 :
4 26 : local M = {}
5 :
6 : -- ============================================================================
7 : -- Private State
8 : -- ============================================================================
9 :
10 26 : local active_timers = {}
11 26 : local active_vim_timers = {}
12 26 : local timer_id_counter = 0
13 26 : local total_timers_created = 0
14 :
15 : -- ============================================================================
16 : -- Helper Functions
17 : -- ============================================================================
18 :
19 : --- Generate unique timer ID
20 : --- @return string
21 : local function generate_timer_id()
22 10 : timer_id_counter = timer_id_counter + 1
23 10 : return "timer_" .. timer_id_counter
24 26 : end
25 :
26 : --- Wrap callback in pcall for safety
27 : --- @param callback function
28 : --- @param timer_id string
29 : --- @return function
30 : local function wrap_callback(callback, timer_id)
31 : return function(...)
32 11 : local ok, err = pcall(callback, ...)
33 11 : if not ok then
34 : vim.notify("Timer callback error [" .. timer_id .. "]: " .. tostring(err), vim.log.levels.ERROR)
35 : end
36 41 : end
37 26 : end
38 :
39 : -- ============================================================================
40 : -- Public API - vim.loop timers
41 : -- ============================================================================
42 :
43 : --- Create a new vim.loop timer with automatic cleanup
44 : --- @param callback function Timer callback
45 : --- @param timeout number Timeout in milliseconds
46 : --- @param repeat_interval number|nil Repeat interval (0 for one-shot)
47 : --- @param timer_id string|nil Optional timer ID for tracking
48 : --- @return table timer Timer handle
49 : --- @return string id Timer ID
50 26 : function M.create_timer(callback, timeout, repeat_interval, timer_id)
51 13 : timer_id = timer_id or generate_timer_id()
52 8 : repeat_interval = repeat_interval or 0
53 :
54 8 : local timer = vim.uv.new_timer()
55 8 : if not timer then
56 : vim.notify("Failed to create timer: " .. timer_id, vim.log.levels.ERROR)
57 : return nil, nil
58 : end
59 :
60 8 : local wrapped_callback = wrap_callback(callback, timer_id)
61 :
62 8 : active_timers[timer_id] = {
63 8 : timer = timer,
64 8 : callback = callback,
65 8 : timeout = timeout,
66 8 : repeat_interval = repeat_interval,
67 8 : }
68 :
69 16 : timer:start(timeout, repeat_interval, vim.schedule_wrap(wrapped_callback))
70 :
71 8 : total_timers_created = total_timers_created + 1
72 :
73 8 : return timer, timer_id
74 26 : end
75 :
76 : --- Stop and cleanup a vim.loop timer
77 : --- @param timer_id string Timer ID
78 26 : function M.stop_timer(timer_id)
79 9 : local timer_data = active_timers[timer_id]
80 9 : if not timer_data then
81 1 : return
82 : end
83 :
84 8 : local timer = timer_data.timer
85 8 : if timer and not timer:is_closing() then
86 8 : timer:stop()
87 8 : timer:close()
88 : end
89 :
90 8 : active_timers[timer_id] = nil
91 34 : end
92 :
93 : --- Check if timer is active
94 : --- @param timer_id string Timer ID
95 : --- @return boolean
96 26 : function M.is_timer_active(timer_id)
97 6 : return active_timers[timer_id] ~= nil
98 26 : end
99 :
100 : -- ============================================================================
101 : -- Public API - vim.fn.timer_start timers
102 : -- ============================================================================
103 :
104 : --- Create a vim.fn.timer_start timer with automatic cleanup
105 : --- @param callback function Timer callback
106 : --- @param delay number Delay in milliseconds
107 : --- @param timer_id string|nil Optional timer ID for tracking
108 : --- @return number|nil timer_handle Timer handle
109 : --- @return string|nil id Timer ID
110 26 : function M.create_vim_timer(callback, delay, timer_id)
111 27 : timer_id = timer_id or generate_timer_id()
112 :
113 44 : local wrapped_callback = wrap_callback(function()
114 11 : callback()
115 11 : active_vim_timers[timer_id] = nil
116 33 : end, timer_id)
117 :
118 24 : local timer_handle = vim.fn.timer_start(delay, wrapped_callback)
119 :
120 22 : if timer_handle == -1 then
121 : vim.notify("Failed to create vim timer: " .. timer_id, vim.log.levels.ERROR)
122 : return nil, nil
123 : end
124 :
125 22 : active_vim_timers[timer_id] = {
126 22 : handle = timer_handle,
127 22 : callback = callback,
128 22 : delay = delay,
129 22 : }
130 :
131 22 : total_timers_created = total_timers_created + 1
132 :
133 22 : return timer_handle, timer_id
134 26 : end
135 :
136 : --- Stop a vim.fn.timer_start timer
137 : --- @param timer_id string Timer ID
138 26 : function M.stop_vim_timer(timer_id)
139 12 : local timer_data = active_vim_timers[timer_id]
140 12 : if not timer_data then
141 1 : return
142 : end
143 :
144 14 : pcall(vim.fn.timer_stop, timer_data.handle)
145 11 : active_vim_timers[timer_id] = nil
146 37 : end
147 :
148 : --- Check if vim timer is active
149 : --- @param timer_id string Timer ID
150 : --- @return boolean
151 26 : function M.is_vim_timer_active(timer_id)
152 50 : return active_vim_timers[timer_id] ~= nil
153 26 : end
154 :
155 : -- ============================================================================
156 : -- Cleanup Functions
157 : -- ============================================================================
158 :
159 : --- Stop all active timers
160 26 : function M.stop_all_timers()
161 42 : for timer_id, _ in pairs(active_timers) do
162 7 : M.stop_timer(timer_id)
163 : end
164 :
165 42 : for timer_id, _ in pairs(active_vim_timers) do
166 7 : M.stop_vim_timer(timer_id)
167 : end
168 61 : end
169 :
170 : --- Get count of active timers
171 : --- @return number loop_timers Number of vim.loop timers
172 : --- @return number vim_timers Number of vim.fn timers
173 26 : function M.get_timer_count()
174 4 : local loop_count = 0
175 4 : local vim_count = 0
176 :
177 6 : for _, _ in pairs(active_timers) do
178 2 : loop_count = loop_count + 1
179 : end
180 :
181 6 : for _, _ in pairs(active_vim_timers) do
182 2 : vim_count = vim_count + 1
183 : end
184 :
185 4 : return loop_count, vim_count
186 26 : end
187 :
188 : --- Get timer statistics for debugging
189 : --- @return table stats Timer statistics
190 26 : function M.get_stats()
191 1 : return {
192 2 : loop_timers = vim.tbl_keys(active_timers),
193 2 : vim_timers = vim.tbl_keys(active_vim_timers),
194 1 : total_created = total_timers_created,
195 1 : }
196 26 : end
197 :
198 : --- Reset timer manager state (for testing)
199 26 : function M.reset()
200 32 : M.stop_all_timers()
201 32 : timer_id_counter = 0
202 32 : total_timers_created = 0
203 58 : end
204 :
205 : -- ============================================================================
206 : -- Autocmd Setup
207 : -- ============================================================================
208 :
209 : --- Setup automatic timer cleanup on Neovim exit
210 26 : function M.setup_cleanup()
211 4 : vim.api.nvim_create_autocmd("VimLeavePre", {
212 2 : group = vim.api.nvim_create_augroup("YodaTimerCleanup", { clear = true }),
213 : desc = "Cleanup all active timers before Neovim exits",
214 : callback = function()
215 : M.stop_all_timers()
216 2 : end,
217 : })
218 28 : end
219 :
220 26 : return M
|