diff --git a/pkg/gui/file_watching.go b/pkg/gui/file_watching.go new file mode 100644 index 000000000..90fb32639 --- /dev/null +++ b/pkg/gui/file_watching.go @@ -0,0 +1,64 @@ +package gui + +import ( + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/jesseduffield/lazygit/pkg/commands" +) + +// NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often +// TODO: consider watching the whole directory recursively (could be more expensive) +func (gui *Gui) watchFilesForChanges() { + var err error + gui.fileWatcher, err = fsnotify.NewWatcher() + if err != nil { + gui.Log.Error(err) + return + } + go func() { + for { + select { + // watch for events + case event := <-gui.fileWatcher.Events: + if event.Op == fsnotify.Chmod { + // for some reason we pick up chmod events when they don't actually happen + continue + } + // only refresh if we're not already + if !gui.State.IsRefreshingFiles { + if err := gui.refreshFiles(); err != nil { + err = gui.createErrorPanel(gui.g, err.Error()) + if err != nil { + gui.Log.Error(err) + } + } + } + + // watch for errors + case err := <-gui.fileWatcher.Errors: + if err != nil { + gui.Log.Warn(err) + } + } + } + }() +} + +func (gui *Gui) addFilesToFileWatcher(files []*commands.File) error { + // watch the files for changes + dirName, err := os.Getwd() + if err != nil { + return err + } + + for _, file := range files { + if err := gui.fileWatcher.Add(filepath.Join(dirName, file.Name)); err != nil { + // swallowing errors here because it doesn't really matter if we can't watch a file + gui.Log.Warn(err) + } + } + + return nil +} diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index af021c132..ae3328eed 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -105,6 +105,13 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo } func (gui *Gui) refreshFiles() error { + gui.State.RefreshingFilesMutex.Lock() + gui.State.IsRefreshingFiles = true + defer func() { + gui.State.IsRefreshingFiles = false + gui.State.RefreshingFilesMutex.Unlock() + }() + selectedFile, _ := gui.getSelectedFile(gui.g) filesView := gui.getFilesView() @@ -126,7 +133,7 @@ func (gui *Gui) refreshFiles() error { } fmt.Fprint(filesView, list) - if filesView == g.CurrentView() { + if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && gui.State.Context == "merging") { newSelectedFile, _ := gui.getSelectedFile(gui.g) alreadySelected := newSelectedFile.Name == selectedFile.Name return gui.handleFileSelect(g, filesView, alreadySelected) @@ -387,6 +394,11 @@ func (gui *Gui) refreshStateFiles() error { // get files to stage files := gui.GitCommand.GetStatusFiles() gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files) + + if err := gui.addFilesToFileWatcher(files); err != nil { + return err + } + gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files)) return gui.updateWorkTreeState() } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 982e1879c..a3bcb1666 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/fsnotify/fsnotify" "github.com/go-errors/errors" // "strings" @@ -79,6 +80,7 @@ type Gui struct { statusManager *statusManager credentials credentials waitForIntro sync.WaitGroup + fileWatcher *fsnotify.Watcher } // for now the staging panel state, unlike the other panel states, is going to be @@ -145,22 +147,24 @@ type panelStates struct { } type guiState struct { - Files []*commands.File - Branches []*commands.Branch - Commits []*commands.Commit - StashEntries []*commands.StashEntry - CommitFiles []*commands.CommitFile - DiffEntries []*commands.Commit - MenuItemCount int // can't store the actual list because it's of interface{} type - PreviousView string - Platform commands.Platform - Updating bool - Panels *panelStates - WorkingTreeState string // one of "merging", "rebasing", "normal" - Context string // important not to set this value directly but to use gui.changeContext("new context") - CherryPickedCommits []*commands.Commit - SplitMainPanel bool - RetainOriginalDir bool + Files []*commands.File + Branches []*commands.Branch + Commits []*commands.Commit + StashEntries []*commands.StashEntry + CommitFiles []*commands.CommitFile + DiffEntries []*commands.Commit + MenuItemCount int // can't store the actual list because it's of interface{} type + PreviousView string + Platform commands.Platform + Updating bool + Panels *panelStates + WorkingTreeState string // one of "merging", "rebasing", "normal" + Context string // important not to set this value directly but to use gui.changeContext("new context") + CherryPickedCommits []*commands.Commit + SplitMainPanel bool + RetainOriginalDir bool + IsRefreshingFiles bool + RefreshingFilesMutex sync.Mutex } // for now the split view will always be on @@ -204,6 +208,8 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma statusManager: &statusManager{}, } + gui.watchFilesForChanges() + gui.GenerateSentinelErrors() return gui, nil @@ -786,6 +792,8 @@ func (gui *Gui) RunWithSubprocesses() error { } } + gui.fileWatcher.Close() + break } else if err == gui.Errors.ErrSwitchRepo { continue