aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-27 07:58:28 +0000
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-27 07:58:28 +0000
commit28014dae7034fff49f5740b013a426aade7873f3 (patch)
tree5943605789c290b9dbe927520f5cc52527866b58
parentd8bbeedd275cea4765555c731b265ce7757ad9e5 (diff)
Coalesce idle-timer flushes in projectile-cache-current-fileHEADmaster
Each call to `projectile-cache-current-file' under persistent caching scheduled a fresh 30-second idle timer that closed over a snapshot of the file list at scheduling time. Opening N files in a session queued N timers; once Emacs went idle they all fired and serialized the cache N times, with the earlier ones writing stale (shorter) lists. Maintain a per-project pending timer in `projectile--pending-cache-flush-timers' instead, cancelling and rescheduling on each new file. The fired callback re-reads the current in-memory cache, so the disk write reflects the final state rather than whichever snapshot the last timer captured.
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el34
2 files changed, 27 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b40474d..eeccf95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@
* Fix `projectile--other-extension-files` sort comparator ignoring its second argument, producing undefined ordering; replaced with a stable partition.
* Fix `projectile-toggle-project-read-only` operating on the wrong buffer after `add-dir-local-variable` by wrapping in `save-selected-window`.
* Fix `projectile-cache-current-file` calling `projectile-project-root` twice instead of reusing the already-resolved value.
+* Fix `projectile-cache-current-file` queueing one idle timer per opened file, each capturing a stale snapshot of the file list. With persistent caching, opening many files in a session would result in N redundant disk writes after Emacs went idle. A pending flush is now coalesced per project and reads the latest in-memory cache at fire time.
* Fix `projectile-load-project-cache` not recording a cache time, which combined with `projectile-files-cache-expire` made the TTL check immediately re-evict freshly loaded data — every call ended up re-reading the cache file from disk and the data was never reindexed. The cache file's mtime is now used to seed `projectile-projects-cache-time`.
* Fix `projectile-load-project-cache` storing nil in cache on corrupt/empty cache files, preventing future reload attempts.
* Fix `projectile-purge-dir-from-cache` only updating the in-memory cache; with persistent caching the purged directory's files would reappear on the next session. The on-disk cache is now updated as well, matching the behavior of `projectile-purge-file-from-cache`.
diff --git a/projectile.el b/projectile.el
index 773dae1..c792c65 100644
--- a/projectile.el
+++ b/projectile.el
@@ -685,6 +685,11 @@ cache hit requires both DIRCONFIG-PATH and MTIME to match the
current file, so changing `projectile-dirconfig-file' mid-session
naturally invalidates the entry.")
+(defvar projectile--pending-cache-flush-timers (make-hash-table :test 'equal)
+ "Map of project root to a pending idle-timer that will serialize its cache.
+Used by `projectile-cache-current-file' to coalesce rapid file additions
+into a single delayed disk write per project.")
+
(defvar projectile--alien-dirconfig-warned-projects (make-hash-table :test 'equal)
"Set of project roots already warned about alien indexing skipping the dirconfig.")
@@ -1273,6 +1278,23 @@ The cache is created both in memory and on the hard drive."
"Check if FILE is already in PROJECT cache."
(member file (gethash project projectile-projects-cache)))
+(defun projectile--schedule-cache-flush (project)
+ "Arrange for PROJECT's in-memory cache to be serialized after Emacs is idle.
+A pending flush for the same PROJECT is cancelled and rescheduled, so that
+adding several files in quick succession only results in a single disk write,
+and the write always uses the latest in-memory contents."
+ (when-let* ((existing (gethash project projectile--pending-cache-flush-timers)))
+ (cancel-timer existing))
+ (puthash project
+ (run-with-idle-timer
+ 30 nil
+ (lambda ()
+ (remhash project projectile--pending-cache-flush-timers)
+ (projectile-serialize
+ (gethash project projectile-projects-cache)
+ (projectile-project-cache-file project))))
+ projectile--pending-cache-flush-timers))
+
;;;###autoload
(defun projectile-cache-current-file ()
"Add the currently visited file to the cache."
@@ -1286,16 +1308,12 @@ The cache is created both in memory and on the hard drive."
(unless (or (projectile-file-cached-p current-file current-project)
(projectile-ignored-directory-p (file-name-directory abs-current-file))
(projectile-ignored-file-p abs-current-file))
- (let ((project-files (cons current-file (gethash current-project projectile-projects-cache)))
- (cache-file (projectile-project-cache-file current-project)))
+ (let ((project-files (cons current-file (gethash current-project projectile-projects-cache))))
(puthash current-project project-files projectile-projects-cache)
- ;; we serialize the cache with an idle time to avoid freezing the UI
- ;; immediately after the new file was created
+ ;; Defer the disk write until Emacs is idle to avoid freezing the
+ ;; UI immediately after the new file was created.
(when (projectile-persistent-cache-p)
- (run-with-idle-timer
- 30
- nil
- 'projectile-serialize project-files cache-file)))
+ (projectile--schedule-cache-flush current-project)))
(message "File %s added to project %s cache."
(propertize current-file 'face 'font-lock-keyword-face)
(propertize current-project 'face 'font-lock-keyword-face)))))))