mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
fix(tui): gate full-view read errors on snapshot validity
renderFileViewFull returned renderedContent whenever hasError was set,
before the snapshot-validity check that governs every other completed
snapshot. A /btw fork of a full view that had settled on a read error
invalidates the inherited snapshot and clears renderedContent, but the
error latch survived, so the side pane rendered an empty body instead of
the loading placeholder until its own load landed. Reloads of an errored
view (resize, refresh, theme) likewise kept showing the stale error.
Serve the error snapshot through the same validity predicate as content
snapshots, so a pending or invalidated load falls through to "Loading…".
Error displays still do not count as cache hits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a1887f4b03)
This commit is contained in:
2 files changed
+49
-4
No files matched your search
@@ -602,3 +602,46 @@ func TestBTWLeaveRevokesSideFileLoad(t *testing.T) {
|
||||
t.Fatal("restored parent must keep its snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBTWForkOfErroredFileViewShowsLoadingPlaceholder verifies that a side
|
||||
// surface forked from a full view whose last snapshot was a read error does
|
||||
// not render an empty body while its own load is pending. The inherited error
|
||||
// snapshot is invalidated by the fork, so the side must show the loading
|
||||
// placeholder until its load lands, then the side's own error snapshot.
|
||||
func TestBTWForkOfErroredFileViewShowsLoadingPlaceholder(t *testing.T) {
|
||||
resetFileViewCacheForTest()
|
||||
m := newBTWTestModel(t)
|
||||
m.cwd = t.TempDir()
|
||||
|
||||
// A missing file opens straight in full mode and settles on a read error.
|
||||
m, loadCmd := m.openFileView("missing.go")
|
||||
if loadCmd == nil || m.fileView.mode != fileViewFull {
|
||||
t.Fatal("expected a full-view load for a file without recorded edits")
|
||||
}
|
||||
m = deliverCommandMessages(t, m, loadCmd)
|
||||
if !m.fileView.hasError {
|
||||
t.Fatal("parent full view should settle on a read error")
|
||||
}
|
||||
if got := plainRender(t, m.renderFileViewFull(80)); !strings.Contains(got, "Could not read file") {
|
||||
t.Fatalf("parent should show the read error, got %q", got)
|
||||
}
|
||||
|
||||
side, sideLoadCmd := m.handleBTWCommand("")
|
||||
if !side.btw.active || sideLoadCmd == nil {
|
||||
t.Fatal("expected an active BTW surface with its own pending file load")
|
||||
}
|
||||
if got := plainRender(t, side.renderFileViewFull(80)); !strings.Contains(got, fileViewLoadingPlaceholder) {
|
||||
t.Fatalf("pending side load must show the loading placeholder, got %q", got)
|
||||
}
|
||||
if got := plainRender(t, side.btw.parent.renderFileViewFull(80)); !strings.Contains(got, "Could not read file") {
|
||||
t.Fatalf("hidden parent must keep its own error snapshot, got %q", got)
|
||||
}
|
||||
|
||||
side = deliverCommandMessages(t, side, sideLoadCmd)
|
||||
if side.fileView.loading {
|
||||
t.Fatal("side load should settle")
|
||||
}
|
||||
if got := plainRender(t, side.renderFileViewFull(80)); !strings.Contains(got, "Could not read file") {
|
||||
t.Fatalf("settled side load should show its read error, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -1319,10 +1319,10 @@ func (m model) renderFileViewDiff(width int) string {
|
||||
// this session's diffs added (matched by exact text — an approximation that
|
||||
// tolerates later drift; a stale marker just doesn't highlight).
|
||||
// It is strictly non-blocking and performs O(1) lookup without invoking formatters.
|
||||
// A read error is a completed snapshot like any other: it is shown only while
|
||||
// that snapshot is still the authoritative one, so a pending reload or a fork
|
||||
// that invalidated it falls through to the loading placeholder.
|
||||
func (m model) renderFileViewFull(width int) string {
|
||||
if m.fileView.hasError {
|
||||
return m.fileView.renderedContent
|
||||
}
|
||||
target := m.fileView.path
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(m.cwd, target)
|
||||
@@ -1333,7 +1333,9 @@ func (m model) renderFileViewFull(width int) string {
|
||||
m.fileView.loadedRev >= m.fileView.requiredSourceRev &&
|
||||
m.fileView.loadedGen == defaultFileViewCache.generation() &&
|
||||
m.fileView.loadedToken == m.fileView.lifetimeToken {
|
||||
defaultFileViewCache.recordHit()
|
||||
if !m.fileView.hasError {
|
||||
defaultFileViewCache.recordHit()
|
||||
}
|
||||
return m.fileView.renderedContent
|
||||
}
|
||||
if cached, ok := defaultFileViewCache.peekRenderOnly(target, width, m.fileView.desiredFingerprint, m.fileView.loadedSeq, m.fileView.desiredSeq, m.fileView.loadedRev, m.fileView.requiredSourceRev); ok {
|
||||
|
||||
Reference in new issue
Block a user