@@ -113,6 +113,34 @@ local function download_file_async(url, output_path, progress_callback, callback
113113 local last_progress = 0
114114 local download_completed = false
115115 local final_stats = nil
116+ local progress_100_reported = false
117+ local debounce_timer = nil
118+ local pending_progress = nil
119+
120+ -- Debounced progress callback to prevent rapid updates
121+ local function report_progress (progress_data )
122+ pending_progress = progress_data
123+ -- Clear existing timer
124+ if debounce_timer then
125+ vim .fn .timer_stop (debounce_timer )
126+ end
127+ -- Set new timer to report after 100ms
128+ debounce_timer = vim .fn .timer_start (100 , function ()
129+ if pending_progress and progress_callback then
130+ -- Only report 100% once
131+ if pending_progress .progress == 100 then
132+ if not progress_100_reported then
133+ progress_callback (pending_progress )
134+ progress_100_reported = true
135+ download_completed = true
136+ end
137+ else
138+ progress_callback (pending_progress )
139+ end
140+ pending_progress = nil
141+ end
142+ end )
143+ end
116144
117145 local job_id = vim .fn .jobstart (cmd , {
118146 env = vim .fn .environ (),
@@ -138,16 +166,15 @@ local function download_file_async(url, output_path, progress_callback, callback
138166 }
139167
140168 -- Only report if we haven't completed yet and have valid data
141- if size_total > 0 and progress_callback and not download_completed then
169+ if size_total > 0 and not download_completed then
142170 local progress = math.floor ((size_download / size_total ) * 100 )
171+ -- Cap at 99% to avoid showing 100% multiple times (let on_exit handle final 100%)
172+ progress = math.min (99 , progress )
143173 if progress ~= last_progress then
144174 local speed_mb = speed / 1024 / 1024
145175 local message = string.format (" Downloading backend... %d%% (%.2f MB/s)" , progress , speed_mb )
146- progress_callback ({ progress = progress , message = message })
176+ report_progress ({ progress = progress , message = message })
147177 last_progress = progress
148- if progress >= 100 then
149- download_completed = true
150- end
151178 end
152179 end
153180 end
@@ -156,7 +183,7 @@ local function download_file_async(url, output_path, progress_callback, callback
156183 on_stderr = vim .schedule_wrap (function (_ , data , _ )
157184 -- Parse curl's -# progress bar from stderr for real-time updates
158185 -- Format: "##..." where each # represents ~2% progress (50 # = 100%)
159- if data and # data > 0 and progress_callback and not download_completed then
186+ if data and # data > 0 and not download_completed then
160187 for _ , line in ipairs (data ) do
161188 if line ~= " " then
162189 -- Count # characters in the line
@@ -166,10 +193,10 @@ local function download_file_async(url, output_path, progress_callback, callback
166193 end
167194 -- Simple progress bar has ~50 # characters for 100%
168195 if hash_count > 0 then
169- local estimated_progress = math.min (100 , math.floor ((hash_count / 50 ) * 100 ))
196+ local estimated_progress = math.min (99 , math.floor ((hash_count / 50 ) * 100 ))
170197 -- Only update if progress changed and we haven't completed
171198 if estimated_progress ~= last_progress and estimated_progress < 100 then
172- progress_callback ({
199+ report_progress ({
173200 progress = estimated_progress ,
174201 message = string.format (" Downloading backend... %d%%" , estimated_progress ),
175202 })
@@ -181,6 +208,17 @@ local function download_file_async(url, output_path, progress_callback, callback
181208 end
182209 end ),
183210 on_exit = vim .schedule_wrap (function (_ , exit_code , _ )
211+ -- Stop any pending debounce timer
212+ if debounce_timer then
213+ vim .fn .timer_stop (debounce_timer )
214+ debounce_timer = nil
215+ end
216+ -- Flush any pending progress immediately
217+ if pending_progress and progress_callback and pending_progress .progress ~= 100 then
218+ progress_callback (pending_progress )
219+ pending_progress = nil
220+ end
221+
184222 if exit_code ~= 0 then
185223 Logger .error (" Download failed with exit code: " .. tostring (exit_code ))
186224 if callback then
@@ -201,13 +239,15 @@ local function download_file_async(url, output_path, progress_callback, callback
201239 f :close ()
202240 make_executable (output_path )
203241 -- Report completion only if we haven't already reported 100%
204- if progress_callback and not download_completed then
242+ if progress_callback and not progress_100_reported then
205243 if final_stats and final_stats .size_total > 0 then
206244 local speed_mb = (final_stats .speed or 0 ) / 1024 / 1024
207245 progress_callback ({ progress = 100 , message = string.format (" Download completed! (%.2f MB/s)" , speed_mb ) })
208246 else
209247 progress_callback ({ progress = 100 , message = " Download completed!" })
210248 end
249+ progress_100_reported = true
250+ download_completed = true
211251 end
212252 Logger .notify (" Snap.nvim backend downloaded successfully!" , Logger .LoggerLogLevels .info )
213253 if callback then
@@ -482,45 +522,41 @@ local function move_extracted_files(temp_dir, bin_dir)
482522 local ext = IS_WINDOWS and " .exe" or " "
483523 local release_bin_name = " snap-nvim-" .. plat .. ext
484524 local bin_path = M .get_bin_path ()
485- local playwright_dir = join_paths (bin_dir , " playwright" )
486-
487- local found_binary = false
488- local found_playwright = false
489-
490- -- Look for binary at root of temp directory (archive extracts to root)
491- local temp_binary = join_paths (temp_dir , release_bin_name )
492- if vim .fn .filereadable (temp_binary ) == 1 then
493- -- Move binary to final location
494- vim .fn .rename (temp_binary , bin_path )
495- make_executable (bin_path )
496- found_binary = true
497- end
498525
499- -- Look for playwright directory at root of temp directory
500- local temp_playwright = join_paths (temp_dir , " playwright" )
501- if vim .fn .isdirectory (temp_playwright ) == 1 then
502- -- Remove old playwright directory if it exists
503- if vim .fn .isdirectory (playwright_dir ) == 1 then
504- vim .fn .delete (playwright_dir , " rf" )
526+ local lookup_temp_paths = {
527+ join_paths (temp_dir , release_bin_name ),
528+ join_paths (temp_dir , " node_modules" ),
529+ join_paths (temp_dir , " playwright" ),
530+ }
531+ local lookup_bin_paths = {
532+ bin_path ,
533+ join_paths (bin_dir , " node_modules" ),
534+ join_paths (bin_dir , " playwright" ),
535+ }
536+
537+ for i , temp_lookup in ipairs (lookup_temp_paths ) do
538+ if vim .fn .filereadable (temp_lookup ) == 0 and vim .fn .isdirectory (temp_lookup ) == 0 then
539+ Logger .error (" Expected file or directory not found in extracted archive: " .. temp_lookup )
540+ return false
541+ end
542+ local bin_lookup = lookup_bin_paths [i ]
543+ if vim .fn .filereadable (bin_lookup ) == 1 then
544+ vim .fn .rename (temp_lookup , bin_lookup )
545+ make_executable (bin_lookup )
546+ end
547+ if vim .fn .isdirectory (temp_lookup ) == 1 then
548+ -- Remove old directory if it exists
549+ if vim .fn .isdirectory (bin_lookup ) == 1 then
550+ vim .fn .delete (bin_lookup , " rf" )
551+ end
552+ -- Move directory to final location
553+ vim .fn .rename (temp_lookup , bin_lookup )
505554 end
506- -- Move playwright directory to final location
507- vim .fn .rename (temp_playwright , playwright_dir )
508- found_playwright = true
509555 end
510556
511557 -- Clean up temp directory
512558 vim .fn .delete (temp_dir , " rf" )
513559
514- if not found_binary then
515- Logger .error (" Binary not found in extracted archive" )
516- return false
517- end
518-
519- -- Playwright directory is optional (for backwards compatibility)
520- if found_playwright then
521- Logger .notify (" Playwright bundled with backend" , Logger .LoggerLogLevels .info )
522- end
523-
524560 return true
525561end
526562
0 commit comments