Skip to content

fix: reload preview immediately after watch file modification#2594

Merged
joelim-work merged 2 commits into
gokcehan:masterfrom
valoq:stale-preview
May 29, 2026
Merged

fix: reload preview immediately after watch file modification#2594
joelim-work merged 2 commits into
gokcehan:masterfrom
valoq:stale-preview

Conversation

@valoq

@valoq valoq commented May 22, 2026

Copy link
Copy Markdown
Contributor

When a file is opened by a terminal editor in lf, saving the file and returning to lf will trigger a flicker where the old contents of the text files are shown as preview for a split second, before the new updated content is drawn in the preview pane.
This is caused by checkReg not marking the cache entry, so printReg still printed the old content. Setting reg.loading now fixes printReg and skips the stale output, so the pane stays empty until the new reg lands.

Note: the bug only triggers when using external preview scripts.

Comment thread nav.go
Comment thread app.go
delete(app.nav.regCache, f.path)
if curr := app.nav.currFile(); curr == nil || curr.path != f.path {
delete(app.nav.regCache, f.path)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this logic is needed - even if the modified file is the current file under the cursor, it will delete the cache entry and add it back in the app.ui.loadFile call below. So the end result is that there will be a cache entry with loading = true either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this change, the old preview briefly disappears and reappears between the editor closing and the new preview showing up. Keeping the cache entry avoids that blank moment.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand the old preview disappearing since the cache entry is invalidated, but I don't understand why it would reappear after being deleted. I also don't understand why you would want to keep the stale cache entry instead of just deleting it and adding it back again immediately in app.ui.loadFile afterwards (it happens in the same loop cycle so theoretically it shouldn't create a flicker).

Do you know the exact root cause of this issue, and why this patch solves it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would it reappear after being deleted

It isn't deleted at that point. The stale paint happens before fileChan fires.

After the editor exits, runCmdSync calls loadFile which runs checkReg
checkReg then updates loadTime and queues a refresh but leaves reg.loading=false and reg.lines untouched, so the next draw paints the stale lines. With set watch=true , fsnotify triggers the refresh and shows the new file content after the old one was already displayed, leading to flicker.

@joelim-work joelim-work May 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #2597, runCmdSync calling loadFile will now set reg.loading=true so the old preview will never be shown, and instead the preview will be blank until the new contents are loaded.

I guess there's still the issue where there are two competing mechanisms for reloading the file (the original checkReg and fsnotify). I'm not sure if it's a good idea for fileChan to delete the cache entry then since that could result in a double load, maybe it is sufficient to just call checkReg instead since that will not do anything if the file has already been reloaded after it was modified, something like this:

diff --git a/app.go b/app.go
index da8bd28..d03552d 100644
--- a/app.go
+++ b/app.go
@@ -466,8 +466,11 @@ func (app *app) loop() {
 				dir.sel(name, app.nav.height)
 			}
 
-			delete(app.nav.regCache, f.path)
-			app.ui.loadFile(app, false)
+			if r, ok := app.nav.regCache[f.path]; ok {
+				app.nav.checkReg(r)
+			} else {
+				// handle possible edge case if editor saves via a backup file?
+			}
 			onLoad(app, []string{f.path})
 			app.ui.draw(app.nav)
 		case path := <-app.nav.delChan:

The filecChan handler should trigger for any file, so including logic that depends on the current file seems wrong to me.

@valoq valoq May 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard isn't redundant. With watch=true and a fast save+quit, runCmdSync's loadFile finishes loading the new preview into regCache before app.loop processes the pending fileChan event. Without the guard, the handler then deletes that just-loaded entry and replaces it with an empty loading=true reg, which printReg renders as a blank pane until a second load completes. That blank flash is the flicker. The guard skips the delete when curr.path == f.path, so the loaded entry stays and there is no flash.

You're right that both paths end with a loading=true entry, but only the no-guard path discards an entry that already has real lines.

The flicker only happens when you use "save and quit" like Vim's "ZZ"
when you instead save and then quit after waiting a second, there is no flicker.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so I think we both agree on the fact that runCmdSync triggers loading the new preview and that it finishes loading before processing the fileChan event. My understanding is that when processing the fileChan event, if deleting is skipped then loadFile will call checkReg but that won't do anything since the file hasn't been modified since the reload via runCmdSync, and therefore there is no second load.

So then my question is this: why should the cache entry be deleted in the first place? Deleting makes sense if the file is actually deleted, but a fileChan event represents a file modification so in theory refreshing it via checkReg should be sufficient.

@valoq valoq May 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes the deletion can be removed. fixed now

@joelim-work joelim-work left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good thanks.

Incidentally this fixes a bug where previews for files other than the current file don't get reloaded immediately, only the cache entry is deleted (can be observed by adding a sleep to the previewer).

@joelim-work joelim-work changed the title fix: outdated preview data after file edit fix: reload preview immediately after watch file modification May 29, 2026
@joelim-work joelim-work merged commit 468b321 into gokcehan:master May 29, 2026
32 checks passed
@joelim-work joelim-work added this to the r42 milestone May 29, 2026
@joelim-work joelim-work added the fix Pull requests that fix existing behavior label May 29, 2026
joelim-work added a commit that referenced this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix existing behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants