@@ -15,31 +15,30 @@ final class DiffStore {
1515
1616 private let cache = DiffCache ( )
1717
18- func get( rev: String ? , path: String ) async -> CachedDiff ? {
19- await cache. get ( Self . key ( rev: rev, path: path) )
20- }
21-
22- func set( rev: String ? , path: String , value: CachedDiff ) async {
23- await cache. set ( Self . key ( rev: rev, path: path) , value: value)
24- }
25-
2618 func clear( ) {
2719 Task { await cache. clear ( ) }
2820 }
2921
3022 /// Load a single file's diff. Returns cached if available, otherwise computes and caches.
23+ ///
24+ /// `commitId` is the immutable content hash used as the cache identity. `rev`
25+ /// (the mutable selection revision) is what jj resolves to fetch content, but
26+ /// keying on it would serve stale diffs after an amend/rebase reuses the id.
3127 func loadDiff(
3228 hunk: DiffHunk ,
3329 rev: String ? ,
30+ commitId: String ? = nil ,
3431 repo: JayJayRepo ? ,
3532 compareFromRev: String ? = nil ,
3633 ignoreWhitespace: Bool = false
3734 ) async -> CachedDiff ? {
3835 guard let repo else { return nil }
3936 guard !hunk. isSubmodulePlaceholder else { return nil }
4037
41- let cacheRev = compareFromRev != nil ? " \( compareFromRev!) → \( rev ?? " " ) " : rev
42- let key = Self . key ( rev: cacheRev, path: hunk. path)
38+ let key = Self . key (
39+ commitId: commitId, rev: rev, compareFromRev: compareFromRev,
40+ ignoreWhitespace: ignoreWhitespace, path: hunk. path
41+ )
4342
4443 if let cached = await cache. get ( key) {
4544 return cached
@@ -81,28 +80,35 @@ final class DiffStore {
8180 func preload(
8281 hunks: [ DiffHunk ] ,
8382 rev: String ? ,
83+ commitId: String ? = nil ,
8484 repo: JayJayRepo ? ,
8585 compareFromRev: String ? = nil ,
8686 ignoreWhitespace: Bool = false
8787 ) {
8888 guard let repo, let rev else { return }
89- preloadHunks ( hunks, rev: rev, repo: repo, compareFromRev: compareFromRev, ignoreWhitespace: ignoreWhitespace)
89+ preloadHunks (
90+ hunks, rev: rev, commitId: commitId, repo: repo,
91+ compareFromRev: compareFromRev, ignoreWhitespace: ignoreWhitespace
92+ )
9093 }
9194
9295 // MARK: - Private
9396
9497 private func preloadHunks(
9598 _ hunks: [ DiffHunk ] ,
9699 rev: String ,
100+ commitId: String ? ,
97101 repo: JayJayRepo ,
98102 compareFromRev: String ? ,
99103 ignoreWhitespace: Bool
100104 ) {
101105 Task . detached ( priority: . utility) { [ cache] in
102106 for hunk in hunks {
103107 guard !hunk. isSubmodulePlaceholder else { continue }
104- let cacheRev = compareFromRev != nil ? " \( compareFromRev!) → \( rev) " : rev
105- let key = DiffStore . key ( rev: cacheRev, path: hunk. path)
108+ let key = DiffStore . key (
109+ commitId: commitId, rev: rev, compareFromRev: compareFromRev,
110+ ignoreWhitespace: ignoreWhitespace, path: hunk. path
111+ )
106112 if await cache. get ( key) != nil { continue }
107113
108114 var old = hunk. oldContent ?? " "
@@ -198,24 +204,73 @@ final class DiffStore {
198204 return LoadedFileContent ( oldContent: " " , newContent: " " , oldPreview: nil , newPreview: nil )
199205 }
200206
201- nonisolated private static func key( rev: String ? , path: String ) -> String {
202- " \( rev ?? " " ) | \( path) "
207+ /// Content-addressed cache key: immutable `commitId` (falling back to `rev`),
208+ /// the compare-from side, the whitespace mode, and the path. Whitespace is
209+ /// part of the key because it changes the computed diff for the same content.
210+ nonisolated static func key(
211+ commitId: String ? ,
212+ rev: String ? ,
213+ compareFromRev: String ? ,
214+ ignoreWhitespace: Bool ,
215+ path: String
216+ ) -> String {
217+ let base = ( commitId? . isEmpty == false ? commitId : rev) ?? " "
218+ let identity = compareFromRev. map { " \( $0) → \( base) " } ?? base
219+ return " \( identity) | \( ignoreWhitespace ? " iw " : " " ) | \( path) "
203220 }
204221}
205222
206- /// Thread-safe diff cache actor .
223+ /// Thread-safe LRU diff cache bounded by the total bytes of cached file content .
207224actor DiffCache {
208225 private var entries : [ String : DiffStore . CachedDiff ] = [ : ]
226+ private var order : [ String ] = [ ] // LRU recency; front = least recently used
227+ private var totalBytes = 0
228+ private let budgetBytes : Int
229+
230+ init ( budgetBytes: Int = 64 * 1024 * 1024 ) {
231+ self . budgetBytes = budgetBytes
232+ }
209233
210234 func get( _ key: String ) -> DiffStore . CachedDiff ? {
211- entries [ key]
235+ guard let value = entries [ key] else { return nil }
236+ touch ( key)
237+ return value
212238 }
213239
214240 func set( _ key: String , value: DiffStore . CachedDiff ) {
241+ if let existing = entries [ key] {
242+ totalBytes -= bytes ( existing)
243+ order. removeAll { $0 == key }
244+ }
215245 entries [ key] = value
246+ order. append ( key)
247+ totalBytes += bytes ( value)
248+ evict ( )
216249 }
217250
218251 func clear( ) {
219252 entries. removeAll ( )
253+ order. removeAll ( )
254+ totalBytes = 0
255+ }
256+
257+ private func touch( _ key: String ) {
258+ order. removeAll { $0 == key }
259+ order. append ( key)
260+ }
261+
262+ /// Drop least-recently-used entries until under budget, always keeping the
263+ /// most recent one (so a single oversized file is still cached for its view).
264+ private func evict( ) {
265+ while totalBytes > budgetBytes, order. count > 1 , let oldest = order. first {
266+ order. removeFirst ( )
267+ if let removed = entries. removeValue ( forKey: oldest) {
268+ totalBytes -= bytes ( removed)
269+ }
270+ }
271+ }
272+
273+ private func bytes( _ diff: DiffStore . CachedDiff ) -> Int {
274+ diff. oldContent. utf8. count + diff. newContent. utf8. count
220275 }
221276}
0 commit comments