11package file_browser
22
33import (
4+ "context"
45 "fmt"
56 "os"
67 path2 "path"
@@ -99,21 +100,32 @@ type FileBrowserComponent struct {
99100
100101 selectionMemory * uiutil.SelectionMemory [data.FileBrowserEntry ]
101102 fileWatcher * util.FileWatcher
103+
104+ diffLoader * uiutil.DebouncedLoader
105+ refreshLoader * uiutil.DebouncedLoader
102106}
103107
104108func NewFileBrowser (application * tview.Application ) * FileBrowserComponent {
105- tableContainer := createFileBrowserTable (application )
106-
107109 fileBrowser := & FileBrowserComponent {
108110 Events : util .NewEmitter [Event ](),
109111
110112 application : application ,
111113
112114 selectionMemory : uiutil .NewSelectionMemory [data.FileBrowserEntry ](),
113-
114- tableContainer : tableContainer ,
115115 }
116116
117+ fileBrowser .diffLoader = uiutil .NewDebouncedLoader (application , func () {
118+ for _ , entry := range fileBrowser .tableContainer .GetEntries () {
119+ if entry != nil && entry .IsLoading {
120+ fileBrowser .tableContainer .UpdateEntry (entry )
121+ }
122+ }
123+ })
124+
125+ fileBrowser .refreshLoader = uiutil .NewDebouncedLoader (application , func () {})
126+
127+ fileBrowser .tableContainer = fileBrowser .createFileBrowserTable (application )
128+
117129 fileBrowser .createLayout ()
118130 fileBrowser .setupTable ()
119131
@@ -203,10 +215,14 @@ func (fileBrowser *FileBrowserComponent) Focus() {
203215 fileBrowser .application .SetFocus (fileBrowser .layout )
204216}
205217
206- func (fileBrowser * FileBrowserComponent ) computeTableEntries () ([]* data.FileBrowserEntry , error ) {
218+ func (fileBrowser * FileBrowserComponent ) computeTableEntries (ctx context. Context , previousDiffs map [ string ]diff_state. DiffState ) ([]* data.FileBrowserEntry , error ) {
207219 path := fileBrowser .path
208220 snapshotEntry := fileBrowser .currentSnapshot
209221
222+ if ctx .Err () != nil {
223+ return nil , ctx .Err ()
224+ }
225+
210226 // list files in current path
211227 realFiles , err := util .ListFilesIn (path )
212228 if err != nil {
@@ -224,6 +240,9 @@ func (fileBrowser *FileBrowserComponent) computeTableEntries() ([]*data.FileBrow
224240
225241 // add entries for files which are present on the "real" location (and possibly within a snapshot as well)
226242 for _ , realFilePath := range realFiles {
243+ if ctx .Err () != nil {
244+ return nil , ctx .Err ()
245+ }
227246 _ , realFileName := path2 .Split (realFilePath )
228247 realFileStat , err := os .Lstat (realFilePath )
229248 if err != nil {
@@ -264,6 +283,9 @@ func (fileBrowser *FileBrowserComponent) computeTableEntries() ([]*data.FileBrow
264283 if snapshotEntry != nil {
265284 // add remaining entries for files which are only present in the snapshot
266285 for _ , snapshotFilePath := range snapshotFilePaths {
286+ if ctx .Err () != nil {
287+ return nil , ctx .Err ()
288+ }
267289 _ , snapshotFileName := path2 .Split (snapshotFilePath )
268290
269291 statSnap , err := os .Lstat (snapshotFilePath )
@@ -286,7 +308,12 @@ func (fileBrowser *FileBrowserComponent) computeTableEntries() ([]*data.FileBrow
286308 }
287309
288310 for _ , entry := range fileEntries {
289- entry .DiffState = fileBrowser .determineDiffState (entry , snapshotEntry )
311+ if oldState , exists := previousDiffs [entry .GetRealPath ()]; exists {
312+ entry .DiffState = oldState
313+ } else {
314+ entry .DiffState = diff_state .Unknown
315+ }
316+ entry .IsLoading = true
290317 }
291318
292319 return fileEntries , nil
@@ -358,16 +385,12 @@ func (fileBrowser *FileBrowserComponent) goUp() {
358385}
359386
360387func (fileBrowser * FileBrowserComponent ) SetPathWithSelection (newPath string , newSelection string ) {
361- fileBrowser .SetPath (newPath , false )
362-
363- // select the directory entry of the path we were coming from
388+ // Remember the intended selection for the new path before triggering async refresh
364389 parentEntryName := path2 .Base (path2 .Clean (newSelection ))
365- for _ , entry := range fileBrowser .GetEntries () {
366- if entry .Name == parentEntryName {
367- fileBrowser .selectFileEntry (entry )
368- return
369- }
370- }
390+ fakeEntry := & data.FileBrowserEntry {Name : parentEntryName }
391+ fileBrowser .selectionMemory .Remember (newPath , 0 , fakeEntry )
392+
393+ fileBrowser .SetPath (newPath , false )
371394}
372395
373396func (fileBrowser * FileBrowserComponent ) SetPath (newPath string , checkExists bool ) {
@@ -400,7 +423,7 @@ func (fileBrowser *FileBrowserComponent) SetPath(newPath string, checkExists boo
400423 }
401424
402425 fileBrowser .emit (PathChangedEvent {NewPath : newPath })
403- fileBrowser .Refresh ()
426+ fileBrowser .Refresh (false )
404427 }
405428}
406429
@@ -480,21 +503,105 @@ func (fileBrowser *FileBrowserComponent) SetSelectedSnapshot(snapshot *data.Snap
480503 }
481504
482505 fileBrowser .currentSnapshot = snapshot
483- fileBrowser .Refresh ()
506+ fileBrowser .Refresh (true )
484507}
485508
486- func (fileBrowser * FileBrowserComponent ) Refresh () {
487- fileBrowser .showMessage (status_message .NewInfoStatusMessage ("Refreshing..." ))
509+ func (fileBrowser * FileBrowserComponent ) startAsyncDiffCalculation () {
510+ if fileBrowser .diffLoader != nil {
511+ fileBrowser .diffLoader .Cancel ()
512+ }
513+
514+ snapshotEntry := fileBrowser .currentSnapshot
515+ entriesToProcess := slices .Clone (fileBrowser .tableContainer .GetEntries ())
516+
517+ if len (entriesToProcess ) == 0 {
518+ return
519+ }
520+
521+ ctx , seq := fileBrowser .diffLoader .Start ()
522+
523+ go func () {
524+ defer fileBrowser .diffLoader .Stop (seq )
525+
526+ // Debounce rapid scrolling
527+ select {
528+ case <- ctx .Done ():
529+ return
530+ case <- time .After (50 * time .Millisecond ):
531+ }
532+
533+ // Preemptively set loading state in case computation takes a while
534+ fileBrowser .application .QueueUpdate (func () {
535+ if ! fileBrowser .diffLoader .IsCurrentSequence (seq ) {
536+ return
537+ }
538+ for _ , entry := range entriesToProcess {
539+ if entry != nil {
540+ entry .IsLoading = true
541+ fileBrowser .tableContainer .UpdateEntry (entry )
542+ }
543+ }
544+ })
545+
546+ type diffResult struct {
547+ entry * data.FileBrowserEntry
548+ state diff_state.DiffState
549+ }
550+ var batch []diffResult
551+ lastDrawTime := time .Now ()
552+
553+ pushBatch := func (forceDraw bool ) {
554+ if len (batch ) == 0 {
555+ return
556+ }
557+ batchCopy := batch
558+ batch = nil
559+
560+ updateFunc := func () {
561+ if ! fileBrowser .diffLoader .IsCurrentSequence (seq ) {
562+ return
563+ }
564+ for _ , res := range batchCopy {
565+ res .entry .DiffState = res .state
566+ res .entry .IsLoading = false
567+ fileBrowser .tableContainer .UpdateEntry (res .entry )
568+ }
569+ }
570+
571+ if forceDraw {
572+ fileBrowser .application .QueueUpdateDraw (updateFunc )
573+ } else {
574+ fileBrowser .application .QueueUpdate (updateFunc )
575+ }
576+ }
488577
578+ for i , entry := range entriesToProcess {
579+ if ctx .Err () != nil {
580+ return
581+ }
582+
583+ diffState := fileBrowser .determineDiffState (entry , snapshotEntry )
584+
585+ batch = append (batch , diffResult {entry : entry , state : diffState })
586+
587+ now := time .Now ()
588+ isLast := i == len (entriesToProcess )- 1
589+ // Draw at most once every 50ms to prevent SSH connection flooding
590+ if isLast || now .Sub (lastDrawTime ) > 50 * time .Millisecond {
591+ pushBatch (true )
592+ lastDrawTime = now
593+ } else if len (batch ) >= 10 {
594+ pushBatch (false )
595+ }
596+ }
597+ }()
598+ }
599+
600+ func (fileBrowser * FileBrowserComponent ) Refresh (debounce bool ) {
489601 _ , _ , width , _ := fileBrowser .tableContainer .GetLayout ().GetRect ()
490602 if width == 0 {
491603 width = 80
492604 }
493- // title is " Path: Path: <path> "
494- // theme.CreateTitleText adds 2 spaces.
495- // Box adds borders (2 chars).
496- // "Path: " is 6 chars.
497- // So available is width - 2 (borders) - 2 (spaces) - 6 (prefix) = width - 10.
498605 maxWidth := width - 10
499606 if maxWidth < 20 {
500607 maxWidth = 20
@@ -503,15 +610,44 @@ func (fileBrowser *FileBrowserComponent) Refresh() {
503610 title := fmt .Sprintf ("Path: %s" , fileBrowser .truncatePath (fileBrowser .path , maxWidth ))
504611 fileBrowser .tableContainer .SetTitle (title )
505612
506- entries , err := fileBrowser .computeTableEntries ()
507- if err != nil {
508- fileBrowser .showError (err )
509- } else {
510- fileBrowser .tableContainer .SetData (entries )
511- fileBrowser .restoreSelectionForPath ()
512- fileBrowser .updateFileWatcher ()
613+ previousDiffs := make (map [string ]diff_state.DiffState )
614+ for _ , entry := range fileBrowser .tableContainer .GetEntries () {
615+ if entry != nil {
616+ previousDiffs [entry .GetRealPath ()] = entry .DiffState
617+ }
513618 }
514- fileBrowser .showMessage (status_message .NewInfoStatusMessage ("" ))
619+
620+ ctx , seq := fileBrowser .refreshLoader .Start ()
621+
622+ go func () {
623+ // Debounce rapid calls to Refresh (e.g. from fast scrolling in SnapshotBrowser)
624+ if debounce {
625+ select {
626+ case <- ctx .Done ():
627+ return
628+ case <- time .After (50 * time .Millisecond ):
629+ }
630+ }
631+
632+ entries , err := fileBrowser .computeTableEntries (ctx , previousDiffs )
633+
634+ fileBrowser .application .QueueUpdateDraw (func () {
635+ if ! fileBrowser .refreshLoader .IsCurrentSequence (seq ) {
636+ return
637+ }
638+ if err != nil {
639+ fileBrowser .showError (err )
640+ } else {
641+ fileBrowser .tableContainer .SetData (entries )
642+ fileBrowser .restoreSelectionForPath ()
643+ fileBrowser .updateFileWatcher ()
644+
645+ fileBrowser .startAsyncDiffCalculation ()
646+
647+ fileBrowser .emit (SelectedTableEntryChangedEvent {fileBrowser .GetSelection ()})
648+ }
649+ })
650+ }()
515651}
516652
517653func (fileBrowser * FileBrowserComponent ) truncatePath (path string , maxWidth int ) string {
@@ -634,7 +770,7 @@ func (fileBrowser *FileBrowserComponent) updateFileWatcher() {
634770 }
635771 fileBrowser .fileWatcher = util .NewFileWatcher (path )
636772 action := func (s string ) {
637- fileBrowser .Refresh ()
773+ fileBrowser .Refresh (false )
638774 fileBrowser .application .Draw ()
639775 }
640776 err := fileBrowser .fileWatcher .Watch (action )
@@ -652,11 +788,13 @@ func (fileBrowser *FileBrowserComponent) showDialog(d dialog.Dialog, actionHandl
652788}
653789
654790func (fileBrowser * FileBrowserComponent ) openColumnSelectionDialog () {
791+ currentActive := fileBrowser .tableContainer .GetColumnSpec ()
792+
655793 d := dialog .NewColumnSelectionDialog (
656794 fileBrowser .application ,
657795 "Configure File Browser Columns" ,
658796 tableColumns ,
659- fileBrowser . tableContainer . GetColumnSpec ( ),
797+ slices . Clone ( currentActive ),
660798 func (activeColumns []* table.Column ) {
661799 fileBrowser .tableContainer .SetActiveColumns (activeColumns )
662800 },
@@ -682,7 +820,7 @@ func (fileBrowser *FileBrowserComponent) runRestoreFileAction(entry *data.FileBr
682820 fileBrowser .showDialog (d , func (action dialog.DialogActionId ) bool {
683821 switch action {
684822 case dialog .DialogCloseActionId :
685- fileBrowser .Refresh ()
823+ fileBrowser .Refresh (false )
686824 }
687825 return false
688826 })
@@ -720,7 +858,7 @@ func (fileBrowser *FileBrowserComponent) showDiff(selection *data.FileBrowserEnt
720858 fileBrowser .showDialog (d , func (action dialog.DialogActionId ) bool {
721859 switch action {
722860 case dialog .DialogCloseActionId :
723- fileBrowser .Refresh ()
861+ fileBrowser .Refresh (false )
724862 }
725863 return false
726864 })
0 commit comments