@@ -271,25 +271,34 @@ end
271271
272272const PATH_cache_lock = Base. ReentrantLock ()
273273const PATH_cache = Set {String} ()
274- cached_PATH_string:: Union{String,Nothing} = nothing
275- function cached_PATH_changed ()
276- global cached_PATH_string
277- @lock (PATH_cache_lock, cached_PATH_string) != = get (ENV , " PATH" , nothing )
274+ PATH_cache_task:: Union{Task,Nothing} = nothing # used for sync in tests
275+ next_cache_update:: Float64 = 0.0
276+ function maybe_spawn_cache_PATH ()
277+ global PATH_cache_task, next_cache_update
278+ @lock PATH_cache_lock begin
279+ PATH_cache_task isa Task && ! istaskdone (PATH_cache_task) && return
280+ time () < next_cache_update && return
281+ PATH_cache_task = Threads. @spawn REPLCompletions. cache_PATH ()
282+ Base. errormonitor (PATH_cache_task)
283+ end
278284end
279- const PATH_cache_finished = Base. Condition () # used for sync in tests
280285
281286# caches all reachable files in PATH dirs
282287function cache_PATH ()
283- global cached_PATH_string
284- path = @lock PATH_cache_lock begin
285- empty! (PATH_cache)
286- cached_PATH_string = get (ENV , " PATH" , nothing )
287- end
288+ path = get (ENV , " PATH" , nothing )
288289 path isa String || return
289290
291+ global next_cache_update
292+
293+ # Calling empty! on PATH_cache would be annoying for async typing hints as completions would temporarily disappear.
294+ # So keep track of what's added this time and at the end remove any that didn't appear this time from the global cache.
295+ this_PATH_cache = Set {String} ()
296+
290297 @debug " caching PATH files" PATH= path
291298 pathdirs = split (path, @static Sys. iswindows () ? " ;" : " :" )
292299
300+ next_yield_time = time () + 0.01
301+
293302 t = @elapsed for pathdir in pathdirs
294303 actualpath = try
295304 realpath (pathdir)
@@ -322,6 +331,7 @@ function cache_PATH()
322331 try
323332 if isfile (joinpath (pathdir, file))
324333 @lock PATH_cache_lock push! (PATH_cache, file)
334+ push! (this_PATH_cache, file)
325335 end
326336 catch e
327337 # `isfile()` can throw in rare cases such as when probing a
@@ -333,10 +343,18 @@ function cache_PATH()
333343 rethrow ()
334344 end
335345 end
336- yield () # so startup doesn't block when -t1
346+ if time () >= next_yield_time
347+ yield () # to avoid blocking typing when -t1
348+ next_yield_time = time () + 0.01
349+ end
337350 end
338351 end
339- notify (PATH_cache_finished)
352+
353+ @lock PATH_cache_lock begin
354+ intersect! (PATH_cache, this_PATH_cache) # remove entries from PATH_cache that weren't found this time
355+ next_cache_update = time () + 10 # earliest next update can run is 10s after
356+ end
357+
340358 @debug " caching PATH files took $t seconds" length (pathdirs) length (PATH_cache)
341359 return PATH_cache
342360end
@@ -380,15 +398,13 @@ function complete_path(path::AbstractString;
380398 end
381399
382400 if use_envpath && isempty (dir)
383- # Look for files in PATH as well. These are cached in `cache_PATH` in a separate task in REPL init.
384- # If we cannot get lock because its still caching just pass over this so that initial
385- # typing isn't laggy. If the PATH string has changed since last cache re-cache it
386- cached_PATH_changed () && Base. errormonitor (Threads. @spawn REPLCompletions. cache_PATH ())
387- if trylock (PATH_cache_lock)
401+ # Look for files in PATH as well. These are cached in `cache_PATH` in an async task to not block typing.
402+ # If we cannot get lock because its still caching just pass over this so that typing isn't laggy.
403+ maybe_spawn_cache_PATH () # only spawns if enough time has passed and the previous caching task has completed
404+ @lock PATH_cache_lock begin
388405 for file in PATH_cache
389406 startswith (file, prefix) && push! (matches, file)
390407 end
391- unlock (PATH_cache_lock)
392408 end
393409 end
394410
0 commit comments